111111111111

Signed-off-by: TRAfoer <lhf190@outlook.com>
This commit is contained in:
2026-01-18 13:11:38 +08:00
parent a31269c632
commit de4e399d78
76 changed files with 3199823 additions and 10619 deletions

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
@@ -1109,5 +1110,7 @@ namespace Ichni.Editor
return fieldVal.Equals(targetVal);
}
#endregion
}
}

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Ichni;
using Ichni.Editor;
using Ichni.RhythmGame;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
public class consoleOnMono : MonoBehaviour
{
void Update()
{
if (Keyboard.current.f7Key.wasPressedThisFrame)
{
StartCoroutine(read());
}
}
public IEnumerator read()
{
string path = Application.streamingAssetsPath + "/1.txt";
string[] lines = File.ReadAllLines(path);
Dictionary<string, ElementFolder> folders = new Dictionary<string, ElementFolder>();
Dictionary<string, Track> tracks = new Dictionary<string, Track>();
ElementFolder FindFolder(string id)
{
if (folders.ContainsKey(id))
{
return folders[id];
}
else
{
throw new Exception($"Folder with ID {id} not found.");
}
}
TMP_Text text = LogWindow.LogText("start..", Color.yellow);
int c = 0;
var rootFolder = ElementFolder.GenerateElement("root", Guid.NewGuid(), new List<string>(), true, null);
foreach (string line in lines)
{
yield return null; // 每行处理后等待一帧,防止卡顿
text.text = $"Processing line {c + 1}/{lines.Length}";
c++;
string[] parts = line.Split(' ');
switch (parts[0])
{
// 在 read() 协程的 foreach 循环 switch (parts[0]) 中添加:
case "FD": // 创建 Folder
{
string folderId = parts[1];
string folderName = parts[2];
// 调用 Ichni 引擎的生成方法 (参考项目中已有的生成逻辑)
ElementFolder newFolder = ElementFolder.GenerateElement(folderName, Guid.NewGuid(), new List<string>(), true, rootFolder);
folders.Add(folderId, newFolder);
}
break;
case "TR": // 仅创建轨道
{
string tId = parts[1];
ElementFolder parentFolder = FindFolder(parts[2]);
Track newTrack = Track.GenerateElement(parts[3], Guid.NewGuid(), new List<string>(), true, parentFolder);
newTrack.trackTimeSubmodule = new TrackTimeSubmoduleMovable(
newTrack, float.Parse(parts[4]), float.Parse(parts[5]), float.Parse(parts[6]), (AnimationCurveType)int.Parse(parts[7])
);
newTrack.trackRendererSubmodule = new TrackRendererSubmoduleAutoOrient(newTrack, false, 0, false, new Vector2(0, 0), new Vector2(1, 1));
tracks.Add(tId, newTrack); // 先加入字典,方便后续 PN 指令查找
}
break;
case "PN": // 逐个添加路径节点
{
string targetTrackId = parts[1];
Track targetTrack = tracks[targetTrackId];
Vector3 pos = new Vector3(float.Parse(parts[2]), float.Parse(parts[3]), float.Parse(parts[4]));
// 生成节点并挂载到轨道
var node = PathNode.GenerateElement(pos.ToString(), Guid.NewGuid(), new List<string>(), true, targetTrack, true);
node.transformSubmodule.originalPosition = pos;
node.transformSubmodule.originalScale = new Vector3(0.1f, 0.1f, 0.1f);
// 标记轨道需要刷新(可选:为了性能可以等所有 PN 完后再统一 Refresh
targetTrack.Refresh();
}
break;
case "SW": // 应用旋转 (旧版相机取反)
{
float startTime = float.Parse(parts[2]);
float endTime = float.Parse(parts[3]);
int curveIndex = int.Parse(parts[4]); // 获取曲线索引
// int curveType = int.Parse(parts[4]); // 如果有曲线系统可以应用
// 提取取反后的旋转数值
Vector3 startRot = new Vector3(float.Parse(parts[5]), float.Parse(parts[6]), float.Parse(parts[7]));
Vector3 endRot = new Vector3(float.Parse(parts[8]), float.Parse(parts[9]), float.Parse(parts[10]));
ElementFolder targetFolder = FindFolder(parts[1]);
if (targetFolder == null) continue;
if (targetFolder.childElementList.FirstOrDefault(i => i is Swirl) == null)
{
Swirl.GenerateElement("swirl", Guid.NewGuid(), new List<string>(), true, targetFolder,
new FlexibleFloat(
new List<AnimatedFloat> { new(startTime, endTime, startRot.x, endRot.x, (AnimationCurveType)curveIndex) })
, new FlexibleFloat(
new List<AnimatedFloat> { new(startTime, endTime, startRot.x, endRot.x, (AnimationCurveType)curveIndex) })
, new FlexibleFloat(
new List<AnimatedFloat> { new(startTime, endTime, startRot.x, endRot.x, (AnimationCurveType)curveIndex) })
);
}
else
{
Swirl swirl = targetFolder.childElementList.First(i => i is Swirl) as Swirl;
swirl.eulerAngleX.Add(new AnimatedFloat(
startTime, endTime, startRot.x, endRot.x, (AnimationCurveType)curveIndex
));
swirl.eulerAngleY.Add(new AnimatedFloat(
startTime, endTime, startRot.y, endRot.y, (AnimationCurveType)curveIndex
));
swirl.eulerAngleZ.Add(new AnimatedFloat(
startTime, endTime, startRot.z, endRot.z, (AnimationCurveType)curveIndex
));
}
}
break;
}
}
text.text = "Completed!";
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: ab1e04d0e4926e748895c81bd8791147
guid: b21947a05ccd4a24789e2de2f396b8f8
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Lean.Pool;
using TMPro;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;
@@ -33,7 +34,12 @@ namespace Ichni.Editor
{
EditorManager.instance.uiManager.mainPage.logWindow.AddLog(text, color);
}
public static TMP_Text LogText(string text, Color color = default)
{
LogWindow logWindow = EditorManager.instance.uiManager.mainPage.logWindow;
logWindow.AddLog(text, color);
return logWindow.logTexts[0].logText;
}
private void AddLog(string text, Color color = default)
{
LogText logText = LeanPool.Spawn(logTextPrefab, textRect).GetComponent<LogText>();

View File

@@ -55,7 +55,7 @@ namespace Ichni.RhythmGame
}
SetParent(parentElement);
EditorManager.instance.uiManager.hierarchy.GenerateTab(this, parentElement);
if (parentElement == null || parentElement.connectedTab != null) EditorManager.instance.uiManager.hierarchy.GenerateTab(this, parentElement);
gameObject.name = elementName;
//GameManager.beatMapContainer.beatMapElementList.Add(this);
//serialNumber = totalSerialNumber++;

View File

@@ -16,7 +16,7 @@ namespace Ichni.RhythmGame
public Material skyboxMaterial;
public string backgroundSpriteName;
public Sprite backgroundSprite;
public SkyboxSubsetter skyboxSubsetter;
public static BackgroundSetter GenerateElement(string elementName, Guid id, List<string> tags,
@@ -28,7 +28,7 @@ namespace Ichni.RhythmGame
LogWindow.Log("There is already a Background Setter in the scene.", Color.red);
return null;
}
BackgroundSetter backgroundSetter = Instantiate(EditorManager.instance.basePrefabs.emptyObject)
.AddComponent<BackgroundSetter>();
EditorManager.instance.backgroundSetter = backgroundSetter;
@@ -39,7 +39,7 @@ namespace Ichni.RhythmGame
backgroundSetter.backgroundSpriteName = backgroundSpriteName;
return backgroundSetter;
}
public override void Refresh()
{
EditorManager.instance.backgroundController.EnableBackground(!useSkybox);
@@ -61,33 +61,79 @@ namespace Ichni.RhythmGame
matchedBM = new BackgroundSetter_BM(elementName, elementGuid, tags, null,
useSkybox, skyboxThemeBundleName, skyboxMaterialName, backgroundSpriteName);
}
// 新增:用于在 Inspector 中显示的列表数据
[HideInInspector] private List<string> themeBundleListForSelection;
[HideInInspector] private List<string> skyboxNameListForSelection;
private void UpdateSelectionLists()
{
themeBundleListForSelection = ThemeBundleManager.instance.loadedThemeBundleList.ConvertAll(x => x.themeBundleName);
skyboxNameListForSelection = new List<string>();
if (!string.IsNullOrEmpty(skyboxThemeBundleName) &&
ThemeBundleManager.instance.TryGetThemeBundle(skyboxThemeBundleName, out ThemeBundle themeBundle))
{
skyboxNameListForSelection = themeBundle.assetList_Material.ConvertAll(x => x.name);
}
}
public override void SetUpInspector()
{
IHaveInspection inspector = EditorManager.instance.uiManager.inspector;
Inspector inspectorMain = EditorManager.instance.uiManager.inspector; // 引用主检查器用于刷新
var container = inspector.GenerateContainer("Background Setter");
var backgroundSettings = container.GenerateSubcontainer(3);
// 1. 开关
var useSkyboxToggle = inspector.GenerateToggle(this, backgroundSettings, "Use Skybox", nameof(useSkybox));
var skyboxThemeBundleField = inspector.GenerateInputField(this, backgroundSettings, "Skybox Theme Bundle", nameof(skyboxThemeBundleName));
var skyboxMaterialNameField = inspector.GenerateInputField(this, backgroundSettings, "Skybox Material", nameof(skyboxMaterialName));
// 刷新可选列表
UpdateSelectionLists();
// 2. 天空盒资源包下拉框 (同步 SkyboxSubsetter 逻辑)
var themeDropdown = inspector.GenerateDropdown(this, backgroundSettings, "Skybox Theme Bundle",
themeBundleListForSelection, nameof(skyboxThemeBundleName));
themeDropdown.AddListenerFunction(() =>
{
UpdateSelectionLists();
inspectorMain.SetInspector(this); // 选择包后刷新 UI 以加载材质列表
});
// 3. 天空盒材质下拉框 (同步 SkyboxSubsetter 逻辑)
var materialDropdown = inspector.GenerateDropdown(this, backgroundSettings, "Skybox Material",
skyboxNameListForSelection, nameof(skyboxMaterialName));
if (string.IsNullOrEmpty(skyboxThemeBundleName) || skyboxNameListForSelection.Count == 0)
{
materialDropdown.dropdown.interactable = false;
}
// 4. 背景图片输入框 (保持原样)
var backgroundSpriteField = inspector.GenerateInputField(this, backgroundSettings, "Background Sprite", nameof(backgroundSpriteName));
// 5. 应用按钮
var applyButton = inspector.GenerateButton(this, backgroundSettings, "Apply", Refresh);
void SetInputFields(bool value) // 根据是否使用Skybox设置输入框的可交互性
// 控制交互性
void SetInputFields(bool value)
{
skyboxThemeBundleField.inputField.interactable = value;
skyboxMaterialNameField.inputField.interactable = value;
themeDropdown.dropdown.interactable = value;
materialDropdown.dropdown.interactable = value && !string.IsNullOrEmpty(skyboxThemeBundleName);
backgroundSpriteField.inputField.interactable = !value;
}
SetInputFields(useSkybox);
useSkyboxToggle.AddListenerFunction(() => EditorManager.instance.backgroundController.EnableBackground(!useSkybox));
useSkyboxToggle.AddListenerFunction(() => SetInputFields(useSkybox));
var generateContainer = inspector.GenerateContainer("Generate");
useSkyboxToggle.AddListenerFunction(() =>
{
EditorManager.instance.backgroundController.EnableBackground(!useSkybox);
SetInputFields(useSkybox);
});
// 生成 Skybox Subsetter 的按钮部分
var generateContainer = inspector.GenerateContainer("Advanced Controls");
var generateSubContainer = generateContainer.GenerateSubcontainer(3);
var generateSkyboxControllerButton = inspector.GenerateButton(this, generateSubContainer, "Skybox Controller", () =>
inspector.GenerateButton(this, generateSubContainer, "Create Skybox Controller", () =>
{
if (skyboxSubsetter == null)
{
@@ -153,7 +199,7 @@ namespace Ichni.RhythmGame
public override GameElement DuplicateBM(GameElement attached)
{
return BackgroundSetter.GenerateElement(elementName, Guid.NewGuid(), tags, false, attached,
return BackgroundSetter.GenerateElement(elementName, Guid.NewGuid(), tags, false, attached,
useSkybox, skyboxThemeBundleName, skyboxMaterialName, backgroundSpriteName);
}
}

View File

@@ -82,7 +82,9 @@ namespace Ichni.RhythmGame
Vector3 normal = Quaternion.Euler(transformSubmodule.currentEulerAngles) * Vector3.up;
float size = transformSubmodule.currentScale.x;
Color color = colorSubmodule.currentBaseColor;
MaterialPropertyBlock materialPropertyBlock = new MaterialPropertyBlock();
materialPropertyBlock.SetColor("_Color", color);
pathNodeSphere.GetComponent<Renderer>()?.SetPropertyBlock(materialPropertyBlock);
transform.localPosition = position;
transform.localRotation = Quaternion.LookRotation(normal);
transform.localScale = Vector3.one * size;

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: f4d99b225afe249449c426683cb3e513
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,326 +0,0 @@
using System.Collections.Generic;
using Ichni.RhythmGame; // 假设 LeanPool 引用在这里或 Lean.Pool 命名空间
using Lean.Pool; // 引入 LeanPool
using TMPro;
using UnityEngine;
using UnityEngine.Serialization;
namespace Ichni.Editor
{
[RequireComponent(typeof(MeshRenderer))]
public class EditorGrid : MonoBehaviour
{
public GridController gridController;
[Header("Camera Settings")]
public Camera sceneCamera;
[Header("Grid Configuration")]
[Tooltip("0 = XZ, 1 = XY, 2 = YZ")]
public int gridPlane = 0;
public float baseScale = 1f;
public float scaleMultiplier = 1f;
public float distanceFactor = 10f;
[Header("Text Settings")]
public bool canShowPositionText;
public bool isShowingPositionText;
public Transform textContainer;
public GameObject positionTextPrefab;
[Tooltip("启用此项可让文字在屏幕上保持固定大小")]
public bool constantScreenSize = true;
[Tooltip("屏幕固定大小的基准系数")]
public float fixedTextSizeFactor => gridController != null ? gridController.fixedTextSizeFactor : 0.02f;
public float textUpdateFrequency = 0.1f;
// --- 运行时状态 ---
private Material gridMaterial;
private float cameraDistance;
public float logScale;
public float gridScale;
// --- 缓存与优化变量 ---
private float lastTextUpdateTime;
private Vector3 lastCameraPosition;
private float lastGridScale;
private Plane gridPlaneCache;
private Vector2 screenCenter;
// 使用 Vector2Int 作为 Key避免浮点数比较误差同时实现 O(1) 查找
// Key: 网格索引 (x index, z index), Value: 文本对象
private Dictionary<Vector2Int, GameObject> activeTexts = new Dictionary<Vector2Int, GameObject>();
// 避免 GC 的复用容器
private HashSet<Vector2Int> requiredIndices = new HashSet<Vector2Int>();
private List<Vector2Int> toRemoveIndices = new List<Vector2Int>();
void Start()
{
sceneCamera = EditorManager.instance.cameraManager.sceneCamera.sceneCamera;
gridMaterial = GetComponent<MeshRenderer>().material;
gridMaterial.SetFloat("_Plane", gridPlane);
// 设置 Shader 线宽
float lineWidthOf3840 = 2;
float lineWidth = lineWidthOf3840 * (Screen.width / 3840f);
gridMaterial.SetFloat("_LineWidth", lineWidth);
screenCenter = new Vector2(Screen.width / 2f, Screen.height / 2f);
UpdateGridPlaneCache();
}
void Update()
{
// 获取当前相机
if (EditorManager.instance?.cameraManager?.currentCamera != null)
sceneCamera = EditorManager.instance.cameraManager.currentCamera;
if (sceneCamera == null) return;
CalculateGridScale();
UpdateShaderParams();
if (canShowPositionText && isShowingPositionText)
{
// 检查是否需要更新网格位置(降低频率)
bool shouldUpdate = Time.time - lastTextUpdateTime >= textUpdateFrequency ||
Vector3.Distance(sceneCamera.transform.position, lastCameraPosition) > gridScale * 0.5f ||
Mathf.Abs(gridScale - lastGridScale) > 0.1f;
if (shouldUpdate)
{
UpdateTextPositions();
lastTextUpdateTime = Time.time;
lastCameraPosition = sceneCamera.transform.position;
lastGridScale = gridScale;
}
}
else if (activeTexts.Count > 0)
{
ClearAllTexts();
}
}
// 使用 LateUpdate 处理文字的朝向和缩放,防止画面抖动
void LateUpdate()
{
if (!canShowPositionText || !isShowingPositionText || activeTexts.Count == 0) return;
UpdateTextVisuals();
}
private void CalculateGridScale()
{
Vector3 camPos = sceneCamera.transform.position;
Vector3 gridPos = transform.position;
cameraDistance = gridPlane switch
{
0 => Mathf.Abs(camPos.y - gridPos.y), // XZ
1 => Mathf.Abs(camPos.z - gridPos.z), // XY
2 => Mathf.Abs(camPos.x - gridPos.x), // YZ
_ => cameraDistance
};
logScale = Mathf.Floor(Mathf.Log(cameraDistance / distanceFactor + 1, 4));
gridScale = baseScale * Mathf.Pow(4, logScale) * scaleMultiplier;
}
private void UpdateShaderParams()
{
gridMaterial.SetFloat("_GridScale", 1 / gridScale);
gridMaterial.SetFloat("_DisappearEndDistance", 100 * gridScale);
}
#region ()
private void UpdateTextPositions()
{
// 每次都获取最新的屏幕中心,保证中心点始终正确
screenCenter = new Vector2(Screen.width / 2f, Screen.height / 2f);
// 1. 射线检测找中心点
Ray sceneCameraRay = sceneCamera.ScreenPointToRay(screenCenter);
if (!gridPlaneCache.Raycast(sceneCameraRay, out float enter))
{
ClearAllTexts();
return;
}
Vector3 centerPoint = sceneCameraRay.GetPoint(enter);
// 距离剔除
if (Vector3.Distance(sceneCamera.transform.position, centerPoint) > 200f) // 增加最大距离
{
ClearAllTexts();
return;
}
float radius = gridScale * 12f; // 可视范围半径
float step = gridScale * 4f; // 网格步长
// 2. 计算当前需要的网格索引范围
requiredIndices.Clear();
// 根据不同的平面,取不同的轴进行计算
float xLocal = 0, yLocal = 0;
switch (gridPlane)
{
case 0: xLocal = centerPoint.x; yLocal = centerPoint.z; break; // XZ
case 1: xLocal = centerPoint.x; yLocal = centerPoint.y; break; // XY
case 2: xLocal = centerPoint.y; yLocal = centerPoint.z; break; // YZ (注意轴向)
}
int minX = Mathf.FloorToInt((xLocal - radius) / step);
int maxX = Mathf.CeilToInt((xLocal + radius) / step);
int minY = Mathf.FloorToInt((yLocal - radius) / step);
int maxY = Mathf.CeilToInt((yLocal + radius) / step);
for (int x = minX; x <= maxX; x++)
{
for (int y = minY; y <= maxY; y++)
{
requiredIndices.Add(new Vector2Int(x, y));
}
}
// 3. 移除不再需要的 (差集)
toRemoveIndices.Clear();
foreach (var kvp in activeTexts)
{
if (!requiredIndices.Contains(kvp.Key))
{
toRemoveIndices.Add(kvp.Key);
}
}
foreach (var key in toRemoveIndices)
{
LeanPool.Despawn(activeTexts[key]);
activeTexts.Remove(key);
}
// 4. 生成新增的
foreach (var index in requiredIndices)
{
if (!activeTexts.ContainsKey(index))
{
SpawnTextAt(index, step);
}
}
}
private void SpawnTextAt(Vector2Int index, float step)
{
GameObject textObj = LeanPool.Spawn(positionTextPrefab, textContainer);
// 计算世界坐标
Vector3 worldPos = Vector3.zero;
float xPos = index.x * step;
float yPos = index.y * step;
// 偏移量:稍微偏离网格线
float offset = gridScale / 6f;
switch (gridPlane)
{
case 0: // XZ
worldPos = new Vector3(xPos + offset, transform.position.y, yPos + offset);
break;
case 1: // XY
worldPos = new Vector3(xPos + offset, yPos + offset, transform.position.z);
break;
case 2: // YZ -> 这里 index.x 对应 Y轴, index.y 对应 Z轴 (视具体需求调整)
worldPos = new Vector3(transform.position.x, xPos + offset, yPos + offset);
break;
}
textObj.transform.position = worldPos;
// 设置文本内容
TMP_Text tmp = textObj.GetComponent<TMP_Text>();
if (tmp) tmp.text = $"({Mathf.RoundToInt(xPos)}, {Mathf.RoundToInt(yPos)})";
activeTexts.Add(index, textObj);
}
/// <summary>
/// 处理朝向摄像机 和 动态大小
/// </summary>
private void UpdateTextVisuals()
{
Vector3 camPos = sceneCamera.transform.position;
foreach (var kvp in activeTexts)
{
GameObject obj = kvp.Value;
if (obj == null) continue;
Transform t = obj.transform;
float dist = Vector3.Distance(camPos, t.position);
// 1. 朝向摄像机
t.LookAt(t.position + sceneCamera.transform.rotation * Vector3.forward,
sceneCamera.transform.rotation * Vector3.up);
// 2. 动态计算大小
if (constantScreenSize)
{
// 简单的透视投影公式:大小 = 距离 * 系数
// 这样距离越远,物体本身越大,但在屏幕上看起来一样大
float finalScale = dist * fixedTextSizeFactor;
t.localScale = new Vector3(finalScale, finalScale, finalScale);
}
else
{
// 原有的逻辑:随网格等级缩放
float staticScale = gridScale * 1.5f;
t.localScale = new Vector3(staticScale, staticScale, staticScale);
}
}
}
public void ClearAllTexts()
{
foreach (var obj in activeTexts.Values)
{
if (obj != null) LeanPool.Despawn(obj);
}
activeTexts.Clear();
}
#endregion
private void UpdateGridPlaneCache()
{
gridPlaneCache = gridPlane switch
{
0 => new Plane(Vector3.up, transform.position),
1 => new Plane(Vector3.forward, transform.position),
2 => new Plane(Vector3.right, transform.position),
_ => new Plane(Vector3.up, transform.position)
};
}
public void SetGridPlane(int planeIndex)
{
gridPlane = planeIndex;
gridMaterial.SetFloat("_Plane", gridPlane);
UpdateGridPlaneCache();
// 切换平面时强制刷新文字
ClearAllTexts();
lastTextUpdateTime = 0;
}
void OnDestroy()
{
if (gridMaterial != null)
{
if (Application.isEditor) DestroyImmediate(gridMaterial);
else Destroy(gridMaterial);
}
// 场景销毁时LeanPool 通常会自动清理,但手动清理引用是个好习惯
activeTexts.Clear();
}
}
}

View File

@@ -1,77 +0,0 @@
using System.Collections.Generic;
using Ichni.RhythmGame;
using Ichni.RhythmGame.Beatmap;
using UnityEngine;
namespace Ichni.Editor
{
public class GridController : MonoBehaviour, IBaseElement
{
public BaseElement_BM matchedBM { get; set; }
public EditorGrid yPlaneGrid;
public EditorGrid xPlaneGrid;
public EditorGrid zPlaneGrid;
[Header("State")]
public bool yPlaneEnabled = true;
public bool xPlaneEnabled = false;
public bool zPlaneEnabled = false;
public bool isYPlaneShowingPositionText = true;
public float fixedTextSizeFactor = 0.1f;
private void Start()
{
RefreshPlanes();
}
public void SetUpInspector()
{
IHaveInspection inspector = EditorManager.instance.uiManager.inspector;
var container = inspector.GenerateContainer("Grid Controller");
var gridSettings = container.GenerateSubcontainer(3);
inspector.GenerateToggle(this, gridSettings, "Y Plane (XZ)", nameof(yPlaneEnabled))
.AddListenerFunction(RefreshPlanes);
inspector.GenerateToggle(this, gridSettings, "X Plane (YZ)", nameof(xPlaneEnabled))
.AddListenerFunction(RefreshPlanes);
inspector.GenerateToggle(this, gridSettings, "Z Plane (XY)", nameof(zPlaneEnabled))
.AddListenerFunction(RefreshPlanes);
inspector.GenerateToggle(this, gridSettings, "Show Y Plane Pos", nameof(isYPlaneShowingPositionText))
.AddListenerFunction(RefreshPlanes);
inspector.GenerateInputField(this, gridSettings, "Fixed Text Size Factor", nameof(fixedTextSizeFactor))
.AddListenerFunction(RefreshPlanes);
}
private void RefreshPlanes()
{
SetGridState(yPlaneGrid, yPlaneEnabled, isYPlaneShowingPositionText);
SetGridState(xPlaneGrid, xPlaneEnabled, false); // 假设其他平面暂时不显示文字
SetGridState(zPlaneGrid, zPlaneEnabled, false);
}
private void SetGridState(EditorGrid grid, bool active, bool showText)
{
if (grid == null) return;
grid.gameObject.SetActive(active);
if (active)
{
grid.canShowPositionText = showText;
grid.isShowingPositionText = showText;
// 如果关闭了文字显示,立刻清理
if (!showText)
{
grid.ClearAllTexts();
}
}
}
}
}

View File

@@ -0,0 +1,94 @@
using System.Collections.Generic;
using Ichni;
using TMPro;
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class SimpleGridController : MonoBehaviour
{
public Camera targetCamera => EditorManager.instance.cameraManager.currentCamera;
[Header("Settings")]
public Material gridMaterial;
public GameObject textPrefab; // 预制体需要挂载 TextMeshPro
public Transform textParent;
[Header("Grid Logic")]
public bool showCoordinates = true;
public float baseGridSize = 1.0f;
[Range(10f, 1000f)] public float drawDistance = 100f;
// --- 内部状态 ---
private MeshRenderer _renderer;
private Material _instancedMat;
private float _currentSpacing = 1.0f;
void Start()
{
InitMesh();
_renderer = GetComponent<MeshRenderer>();
if (gridMaterial)
{
_instancedMat = new Material(gridMaterial);
_renderer.material = _instancedMat;
}
}
void LateUpdate()
{
if (!targetCamera) return;
Vector3 camPos = targetCamera.transform.position;
// 1. 网格跟随相机 (无限大地板)
transform.position = new Vector3(camPos.x, 0, camPos.z);
// 2. 计算层级 (1m / 10m / 100m)
CalculateGridLevel(camPos.y);
// 3. 更新 Shader
if (_instancedMat)
{
_instancedMat.SetFloat("_GridSpacing", _currentSpacing);
_instancedMat.SetFloat("_FadeDist", drawDistance);
}
}
private void CalculateGridLevel(float camHeight)
{
float absH = Mathf.Abs(camHeight);
float step = baseGridSize;
// 简单的自适应逻辑:根据高度决定网格密度
if (absH > 15f) step *= 10f;
if (absH > 150f) step *= 10f;
_currentSpacing = step;
}
private void InitMesh()
{
MeshFilter mf = GetComponent<MeshFilter>();
Mesh mesh = new Mesh();
// 覆盖视野的大 Quad
float size = 2000f;
Vector3[] vertices = new Vector3[4]
{
new Vector3(-size, 0, -size), new Vector3(size, 0, -size),
new Vector3(-size, 0, size), new Vector3(size, 0, size)
};
mesh.vertices = vertices;
mesh.triangles = new int[6] { 0, 2, 1, 2, 3, 1 };
mesh.uv = new Vector2[4] { Vector2.zero, Vector2.right, Vector2.up, Vector2.one };
mesh.RecalculateBounds();
mf.mesh = mesh;
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: dcf702d09b6611648a7df04aa49aa927
guid: 7096654614f573d4b954741439d193c9
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -12,6 +12,10 @@ using Ichni.Editor;
using Ichni.RhythmGame;
using Ichni.RhythmGame.Beatmap;
using TMPro;
using UniRx;
using Unity.VisualScripting;
@@ -74,7 +78,6 @@ namespace ichni.RhythmGame // [修复] 修正命名空间首字母大写,符
line.startColor = line.endColor = color;
}
// 工具方法:刷新两个线条之间的 MeshCollider
private void RefreshMeshCollider(LineRenderer lineA, LineRenderer lineB)
{
MeshCollider col = lineA.GetComponent<MeshCollider>();
@@ -150,7 +153,7 @@ namespace ichni.RhythmGame // [修复] 修正命名空间首字母大写,符
GameObject obj = Instantiate(EditorManager.instance.basePrefabs.emptyObject, this.transform);
LineRenderer lr = obj.AddComponent<LineRenderer>();
Color color = (b % 1f <= 0.01f) ? Color.green : Color.cyan;
Color color = (Mathf.Abs(b - Mathf.Round(b)) <= 0.01f) ? Color.green : Color.cyan;
SetupLineRenderer(lr, p0, p1, color);
float bi = (trackPercent >= 1f) ? EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackEndTime) : b;
@@ -201,54 +204,29 @@ namespace ichni.RhythmGame // [修复] 修正命名空间首字母大写,符
BeatLines.Add(lr);
}
Color color = (b % 1f <= 0.01f) ? Color.green : Color.cyan;
Color color = (Mathf.Abs(b - Mathf.Round(b)) <= 0.01f) ? Color.green : Color.cyan;
SetupLineRenderer(lr, p0, p1, color, false);
// 同步 Beats 数组,这对 AddNote 很重要
float bi = (trackPercent >= 1f) ? EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackEndTime) : b;
Beats.Add(bi < 0 ? 0f : bi);
index++;
}
// 隐藏多余
for (int i = index; i < BeatLines.Count; i++) BeatLines[i].gameObject.SetActive(false);
// 刷新 Collider
for (int i = 0; i < index - 1; i++) RefreshMeshCollider(BeatLines[i], BeatLines[i + 1]);
}
// void OnDrawGizmos()
// {
// // 调试用,保持原样
// if (BeatLines.Count > 0)
// {
// foreach (var line in BeatLines)
// {
// if (line != null)
// {
// MeshCollider collider = line.GetComponent<MeshCollider>();
// if (collider != null && collider.sharedMesh != null)
// {
// Gizmos.color = new Color(0, 1, 0, 0.3f);
// // DrawMesh 需要传入位置和旋转,因为 Mesh 数据是局部的
// Gizmos.DrawWireMesh(collider.sharedMesh, line.transform.position, line.transform.rotation);
// }
// }
// }
// }
// }
public bool ForceRefresh = false;
void Update()
{
if (ForceRefresh)
{
AdjustBeatLine();
}
if (InputListener.instance.isPointerOverUI) return;
CastRay();
if (IsEnabled && selectedLine != null)
{
DetectNote();
}
if (ForceRefresh)
{
AdjustBeatLine();
}
}
public void CastRay()
@@ -262,44 +240,30 @@ namespace ichni.RhythmGame // [修复] 修正命名空间首字母大写,符
if (Physics.RaycastAll(ray).FirstOrDefault(h => h.collider.CompareTag("LineRenderer")).collider != null)
{
hit = Physics.RaycastAll(ray).First(h => h.collider.CompareTag("LineRenderer"));
if (hit.collider.CompareTag("LineRenderer"))
LineRenderer hoveredLine = hit.collider.GetComponent<LineRenderer>();
if (BeatLines.Contains(hoveredLine))
{
LineRenderer hoveredLine = hit.collider.GetComponent<LineRenderer>();
// [修复] DOTween 逻辑
if (BeatLines.Contains(hoveredLine))
if (Mouse.current.leftButton.wasPressedThisFrame)
{
if (Mouse.current.leftButton.wasPressedThisFrame)
Debug.Log($"Clicked on line area: {hoveredLine.gameObject.name} at {hit.point}");
}
if (selectedLine != hoveredLine)
{
if (selectedLine != null)
{
Debug.Log($"Clicked on line area: {hoveredLine.gameObject.name} at {hit.point}");
}
// 如果悬停到了新的线
if (selectedLine != hoveredLine)
{
// 1. 还原上一条选中的线
if (selectedLine != null)
{
// 杀掉旧的动画
selectedLine.startWidth = 0.05f;
selectedLine.endWidth = 0.05f;
}
// 2. 更新当前选中的线
selectedLine = hoveredLine;
// 3. 对新线开启宽度的 PingPong 动画
selectedLine.startWidth = 0.15f;
selectedLine.endWidth = 0.15f;
selectedLine.startWidth = 0.05f;
selectedLine.endWidth = 0.05f;
}
selectedLine = hoveredLine;
selectedLine.startWidth = 0.15f;
selectedLine.endWidth = 0.15f;
}
}
}
else
{
// [优化] 如果鼠标移到了空白处,应该停止所有高亮
if (selectedLine != null)
{
selectedLine.startWidth = 0.05f;
@@ -335,53 +299,86 @@ namespace ichni.RhythmGame // [修复] 修正命名空间首字母大写,符
float time = EditorManager.instance.songInformation.beatManager.GetTimeFromBeat(
Beats[BeatLines.IndexOf(selectedLine)]
);
Vector3 pos = new Vector3();
NoteBase a = null;
switch (NoteCode)
{
case 0:
Tap a = Tap.GenerateElement("New Tap", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time);
//a.noteVisual.transformSubmodule.originalPosition = new Vector3(isExpand ? (localMousePosition.x / XWidth) : 0f, 0f, 0f);
case 0:
a = Tap.GenerateElement("New Tap", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time);
a.noteVisual.transformSubmodule.Refresh();
pos = a.noteVisual.transform.position;
break;
case 3:
Hold b = Hold.GenerateElement("New Hold", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time, time + 0.5f);
a = Hold.GenerateElement("New Hold", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time, time + 0.5f);
a.noteVisual.transformSubmodule.Refresh();
Observable.NextFrame().Subscribe(_ =>
{
StartCoroutine(DraggingHold((Hold)a));
});
//b.noteVisual.transformSubmodule.originalPosition = new Vector3(isExpand ? (localMousePosition.x / XWidth) : 0f, 0f, 0f);
b.noteVisual.transformSubmodule.Refresh();
pos = b.noteVisual.transform.position;
break;
case 1:
Stay c = Stay.GenerateElement("New Stay", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time);
//c.noteVisual.transformSubmodule.originalPosition = new Vector3(isExpand ? (localMousePosition.x / XWidth) : 0f, 0f, 0f);
c.noteVisual.transformSubmodule.Refresh();
pos = c.noteVisual.transform.position;
a = Stay.GenerateElement("New Stay", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time);
a.noteVisual.transformSubmodule.Refresh();
break;
case 2:
Flick d = Flick.GenerateElement("New Flick", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time, new List<Vector2>());
//d.noteVisual.transformSubmodule.originalPosition = new Vector3(isExpand ? (localMousePosition.x / XWidth) : 0f, 0f, 0f);
d.noteVisual.transformSubmodule.Refresh();
pos = d.noteVisual.transform.position;
a = Flick.GenerateElement("New Flick", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time, new List<Vector2>());
a.noteVisual.transformSubmodule.Refresh();
break;
default:
break;
}
CreateTextHint(pos, $"A Note Added");
Observable.NextFrame().Subscribe(_ =>
{
CreateTextHint(a);
});
}
private void CreateTextHint(Vector3 worldPos, string content)
private IEnumerator DraggingHold(Hold hold)
{
GameObject obj = Instantiate(EditorManager.instance.basePrefabs.emptyObject, this.transform);
LineRenderer lr = obj.AddComponent<LineRenderer>();
lr.startWidth = lr.endWidth = 0.05f;
lr.positionCount = 2;
lr.material = EditorManager.instance.basePrefabs.defaultTrailMaterial;
lr.startColor = lr.endColor = Color.yellow;
lr.SetPosition(0, hold.noteVisual.transform.position);
lr.SetPosition(1, hold.noteVisual.transform.position);
while (Keyboard.current.digit4Key.isPressed && selectedLine != null)
{
yield return null;
try
{
float time = EditorManager.instance.songInformation.beatManager.GetTimeFromBeat(
Beats[BeatLines.IndexOf(selectedLine)]
);
hold.holdEndTime = time < hold.exactJudgeTime ? hold.exactJudgeTime + 0.1f : time;
hold.noteVisual.transformSubmodule.Refresh();
lr.SetPosition(1, (selectedLine.GetPosition(0) + selectedLine.GetPosition(1)) / 2);
}
catch (Exception e)
{
Debug.LogWarning(e);
break;
}
}
yield return null;
Destroy(lr.gameObject);
CreateTextHint(hold);
}
private void CreateTextHint(NoteBase noteBase)
{
// 1. 创建一个新的 GameObject
string content = noteBase.elementName;
Vector3 worldPos = noteBase.noteVisual.transform.position;
GameObject hintObj = new GameObject("NoteHint_" + content);
hintObj.transform.position = worldPos;
// 2. 添加 TextMeshPro 组件 (注意是 TextMeshPro不是 TextMeshProUGUI)
// 因为我们在 3D 空间生成,所以使用非 UI 版本
TextMeshPro text = hintObj.AddComponent<TextMeshPro>();
// 3. 配置文字属性
text.text = content;
text.fontSize = 6; // 根据你的缩放调整大小
@@ -393,6 +390,7 @@ namespace ichni.RhythmGame // [修复] 修正命名空间首字母大写,符
// 5. 动画效果:向上移动 1 个单位,并同时淡出
// 1 秒后执行
hintObj.transform.DOScale(Vector3.one * 1.2f, 1f).SetEase(Ease.OutBack).From(Vector3.zero);
hintObj.transform.DOMoveY(worldPos.y + 1f, 1f);
text.DOFade(0, 1f).OnComplete(() => Destroy(hintObj));
// 7. 让文字始终面向相机 (Billboard 效果)

View File

@@ -26,7 +26,7 @@ namespace Ichni
public EditorSettings editorSettings;
public OperationManager operationManager;
public BackgroundController backgroundController;
public GridController gridController;
public SimpleGridController gridController;
public CameraManager cameraManager;
public NoteManager noteManager;
public Ichni.Editor.PostProcessingManager postProcessingManager;
@@ -189,7 +189,15 @@ namespace Ichni
projectInformation.SetUpInspector();
songInformation.SetUpInspector();
cameraManager.SetUpInspector();
gridController.SetUpInspector();
var oo = inspector.GenerateContainer("Grid");
var p = oo.GenerateSubcontainer(3);
var po = inspector.GenerateToggle(this, p, "Enable Grid", nameof(gridController) + "." + nameof(gridController.gameObject) + ".activeSelf");
var o = inspector.GenerateInputField(p, "Grid Size", (gridController.baseGridSize * 10).ToString());
o.AddListenerFunction(() =>
{
gridController.baseGridSize = float.Parse(o.inputField.text) / 10;
});
var c = inspector.GenerateToggle(this, p, "Show Coordinates", nameof(gridController) + "." + nameof(gridController.showCoordinates));
}