这能叫例行更新吗

Signed-off-by: TRAfoer <lhf190@outlook.com>
This commit is contained in:
2026-02-09 23:10:55 +08:00
parent 77726bcb6c
commit a76f650998
40 changed files with 1323 additions and 866 deletions

View File

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

View File

@@ -0,0 +1,473 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
using Dreamteck;
using Dreamteck.Splines;
using Dreamteck.Splines.Primitives;
using ichni.RhythmGame;
using Ichni;
using Ichni.Editor;
using Ichni.RhythmGame;
using Ichni.RhythmGame.Beatmap;
using TMPro;
using UniRx;
using Unity.VisualScripting;
// [修复] 统一命名空间引用
using UnityEngine;
using UnityEngine.InputSystem;
namespace ichni.RhythmGame // [修复] 修正命名空间首字母大写符合C#规范
{
public partial class FastNoteTracker : GameElement, IBeChangeInExport
{
public Track TrackedTrack { get => parentElement as Track; }
private bool _isEnabled = false;
public float horizonWidth = 5f;
public bool IsEnabled
{
get => _isEnabled;
set
{
_isEnabled = value;
Refresh();
}
}
BaseElement_BM IBeChangeInExport.MatchingExportElement { get => null; set { } }
public List<LineRenderer> BeatLines = new List<LineRenderer>();
public List<float> Beats = new List<float>();
public override void Refresh()
{
base.Refresh();
UpdateBeatLine();
}
public int BeatDiver = 1; // [建议] 变量名修正为 BeatDiver (原本是 Beatdiver)
private LineRenderer selectedLine = null;
// 工具方法:计算线条的两个端点位置
private void CalculateLinePositions(SplineSample sample, out Vector3 pos0, out Vector3 pos1)
{
Vector3 sideOffset = sample.rotation * Vector3.left * horizonWidth;
pos0 = sample.position + sideOffset;
pos1 = sample.position - sideOffset;
}
// 工具方法:配置 LineRenderer 的基础属性
private void SetupLineRenderer(LineRenderer line, Vector3 pos0, Vector3 pos1, Color color, bool refreshWidth = true)
{
line.useWorldSpace = true;
line.positionCount = 2;
line.SetPosition(0, pos0);
line.SetPosition(1, pos1);
if (refreshWidth)
{
line.startWidth = 0.05f;
line.endWidth = 0.05f;
}
line.material = EditorManager.instance.basePrefabs.defaultTrailMaterial;
line.startColor = line.endColor = color;
}
private void RefreshMeshCollider(LineRenderer lineA, LineRenderer lineB)
{
MeshCollider col = lineA.GetComponent<MeshCollider>();
if (col == null) col = lineA.gameObject.AddComponent<MeshCollider>();
Vector3 a1 = lineA.GetPosition(0);
Vector3 a2 = lineA.GetPosition(1);
Vector3 b1 = lineB.GetPosition(0);
Vector3 b2 = lineB.GetPosition(1);
if (Vector3.Distance(a1, b1) < 0.001f)
{
if (col.sharedMesh != null) col.sharedMesh.Clear();
return;
}
// 转换为局部坐标
Vector3[] newVertices = new Vector3[] {
lineA.transform.InverseTransformPoint(a1),
lineA.transform.InverseTransformPoint(a2),
lineA.transform.InverseTransformPoint(b1),
lineA.transform.InverseTransformPoint(b2)
};
Mesh mesh = col.sharedMesh;
if (mesh == null)
{
mesh = new Mesh();
mesh.name = "BeatLineMesh";
}
else
{
// [关键优化]:如果新旧顶点差异极小,则不刷新 Mesh防止物理系统抖动
if (mesh.vertexCount == 4 && Vector3.Distance(mesh.vertices[0], newVertices[0]) < 0.0001f)
{
return;
}
mesh.Clear();
}
mesh.vertices = newVertices;
mesh.triangles = new int[] { 0, 1, 2, 2, 1, 3, 2, 1, 0, 3, 1, 2 };
mesh.RecalculateNormals();
mesh.RecalculateBounds();
col.sharedMesh = mesh;
// 确保 Tag 正确
if (!lineA.CompareTag("LineRenderer")) lineA.tag = "LineRenderer";
}
public void UpdateBeatLine()
{
// 1. 清理
foreach (var line in BeatLines) if (line != null) Destroy(line.gameObject);
BeatLines.Clear();
Beats.Clear();
if (!_isEnabled) return;
// 获取数据源
SplineComputer splineComputer = TrackedTrack.trackPathSubmodule.path;
var trackTime = TrackedTrack.trackTimeSubmodule as TrackTimeSubmoduleMovable;
float beatStart = EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackStartTime);
float beatEnd = EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackEndTime);
// 2. 批量生成
for (float b = beatStart - 1; b <= beatEnd + 1f / BeatDiver; b += 1f / BeatDiver)
{
float timeAtBeat = EditorManager.instance.songInformation.beatManager.GetTimeFromBeat(b);
float trackPercent = Mathf.Clamp01(trackTime.GetTrackPercentRaw(timeAtBeat));
CalculateLinePositions(splineComputer.Evaluate(trackPercent), out Vector3 p0, out Vector3 p1);
GameObject obj = Instantiate(EditorManager.instance.basePrefabs.emptyObject, this.transform);
LineRenderer lr = obj.AddComponent<LineRenderer>();
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;
Beats.Add(bi < 0 ? 0f : bi);
BeatLines.Add(lr);
}
// 3. 批量生成 Collider
for (int i = 0; i < BeatLines.Count - 1; i++) RefreshMeshCollider(BeatLines[i], BeatLines[i + 1]);
}
public void AdjustBeatLine()
{
if (!_isEnabled)
{
foreach (var line in BeatLines) if (line != null) line.gameObject.SetActive(false);
return;
}
SplineComputer splineComputer = TrackedTrack.trackPathSubmodule.path;
var trackTime = TrackedTrack.trackTimeSubmodule as TrackTimeSubmoduleMovable;
float beatStart = EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackStartTime);
float beatEnd = EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackEndTime);
int index = 0;
// 重置 Beats 列表以匹配新的位置
Beats.Clear();
for (float b = beatStart - 1; b <= beatEnd + 1f / BeatDiver; b += 1f / BeatDiver)
{
float timeAtBeat = EditorManager.instance.songInformation.beatManager.GetTimeFromBeat(b);
float trackPercent = Mathf.Clamp01(trackTime.GetTrackPercentRaw(timeAtBeat));
// 如果超出范围,跳过更新(除非你想一直显示)
// 这里沿用你 UpdateBeatLine 的逻辑
CalculateLinePositions(splineComputer.Evaluate(trackPercent), out Vector3 p0, out Vector3 p1);
LineRenderer lr;
if (index < BeatLines.Count)
{
lr = BeatLines[index];
lr.gameObject.SetActive(true);
}
else
{
GameObject obj = Instantiate(EditorManager.instance.basePrefabs.emptyObject, this.transform);
lr = obj.AddComponent<LineRenderer>();
BeatLines.Add(lr);
}
Color color = (Mathf.Abs(b - Mathf.Round(b)) <= 0.01f) ? Color.green : Color.cyan;
SetupLineRenderer(lr, p0, p1, color, false);
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);
for (int i = 0; i < index - 1; i++) RefreshMeshCollider(BeatLines[i], BeatLines[i + 1]);
}
public bool ForceRefresh = false;
void Update()
{
if (ForceRefresh)
{
AdjustBeatLine();
}
if (InputListener.instance.isPointerOverUI) return;
CastRay();
if (IsEnabled && selectedLine != null)
{
DetectNote();
}
}
public void CastRay()
{
if (EditorManager.instance.cameraManager.currentCamera == null) return;
Ray ray = EditorManager.instance.cameraManager.currentCamera.ScreenPointToRay(Mouse.current.position.ReadValue());
RaycastHit hit;
// Debug.DrawRay(ray.origin, ray.direction * 100f, Color.red);
if (Physics.RaycastAll(ray).FirstOrDefault(h => h.collider.CompareTag("LineRenderer")).collider != null)
{
hit = Physics.RaycastAll(ray).First(h => h.collider.CompareTag("LineRenderer"));
LineRenderer hoveredLine = hit.collider.GetComponent<LineRenderer>();
if (BeatLines.Contains(hoveredLine))
{
if (Mouse.current.leftButton.wasPressedThisFrame)
{
Debug.Log($"Clicked on line area: {hoveredLine.gameObject.name} at {hit.point}");
}
if (selectedLine != hoveredLine)
{
if (selectedLine != null)
{
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;
selectedLine.endWidth = 0.05f;
selectedLine = null;
}
}
}
public void InputDetected()
{
// if(Keyboard)
}
public void DetectNote()
{
if (Keyboard.current.digit1Key.wasPressedThisFrame)
AddNote(0);
else if (Keyboard.current.digit2Key.wasPressedThisFrame)
AddNote(1);
else if (Keyboard.current.digit3Key.wasPressedThisFrame)
AddNote(2);
else if (Keyboard.current.digit4Key.wasPressedThisFrame)
AddNote(3);
}
public void AddNote(int NoteCode)
{
if (!EditorManager.instance.useNotePrefab)
{
LogWindow.Log("Please enable \"Note Prefab\" in EditorManager", Color.red);
return;
}
if (selectedLine == null) return;
float time = EditorManager.instance.songInformation.beatManager.GetTimeFromBeat(
Beats[BeatLines.IndexOf(selectedLine)]
);
NoteBase a = null;
switch (NoteCode)
{
case 0:
a = Tap.GenerateElement("New Tap", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time);
a.noteVisual.transformSubmodule.Refresh();
break;
case 3:
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));
});
break;
case 1:
a = Stay.GenerateElement("New Stay", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time);
a.noteVisual.transformSubmodule.Refresh();
break;
case 2:
a = Flick.GenerateElement("New Flick", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time, new List<Vector2>());
a.noteVisual.transformSubmodule.Refresh();
break;
default:
break;
}
Observable.NextFrame().Subscribe(_ =>
{
CreateTextHint(a);
});
}
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; // 根据你的缩放调整大小
text.alignment = TextAlignmentOptions.Center;
text.color = Color.yellow;
// 4. (可选) 设置渲染层级,确保它在轨道上方显示
// text.sortingOrder = 100;
// 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 效果)
// 如果你的相机视角是固定的,可以直接设置旋转
hintObj.transform.LookAt(hintObj.transform.position + Camera.main.transform.rotation * Vector3.forward,
EditorManager.instance.cameraManager.currentCamera.transform.rotation * Vector3.up);
}
}
// 后面的 Inspector 和 Export 代码逻辑暂时不需要大改,只要注意变量名 Beatdiver -> BeatDiver
public partial class FastNoteTracker
{
public BaseElement_BM MatchingExportElement { get => null; set => throw new System.NotImplementedException(); }
public void SaveExportBM() { }
public override void SetUpInspector()
{
base.SetUpInspector();
IHaveInspection inspector = EditorManager.instance.uiManager.inspector;
var container = inspector.GenerateContainer("Fast Note Tracker");
var sub = container.GenerateSubcontainer(2);
inspector.GenerateToggle(this, sub, "Enabled", nameof(IsEnabled));
// 修正变量名
inspector.GenerateInputField(this, sub, "Beat Diver", nameof(BeatDiver));
inspector.GenerateToggle(this, sub, "Force refresh (cost++)", nameof(ForceRefresh));
inspector.GenerateInputField(this, sub, "Horizon Width", nameof(horizonWidth));
}
public static FastNoteTracker GenerateElement(string elementName, Guid id, List<string> tags,
bool isFirstGenerated, GameElement parentElement)
{
FastNoteTracker fastNoteTracker = Instantiate(EditorManager.instance.basePrefabs.emptyObject, parentElement.transform)
.AddComponent<FastNoteTracker>();
if (parentElement is not Track)
{
LogWindow.Log("FastNoteTracker must be a child of Track element.");
return null;
}
fastNoteTracker.Initialize(elementName, id, tags, isFirstGenerated, parentElement);
return fastNoteTracker;
}
public override void Initialize(string elementName, Guid id, List<string> tags, bool isFirstGenerated, GameElement parentElement)
{
base.Initialize(elementName, id, tags, isFirstGenerated, parentElement);
//parentElement.refreshAction += AdjustBeatLine;
}
public override void SaveBM()
{
matchedBM = new FastNoteTracker_BM(elementName, elementGuid, tags, parentElement.elementGuid);
}
}
}
namespace Ichni.RhythmGame.Beatmap
{
public class FastNoteTracker_BM : GameElement_BM
{
public FastNoteTracker_BM(string elementName, Guid id, List<string> tags, Guid attachedElementGuid)
{
this.elementName = elementName;
this.elementGuid = id;
this.tags = tags;
this.attachedElementGuid = attachedElementGuid;
}
public override GameElement DuplicateBM(GameElement attached)
{
return FastNoteTracker.GenerateElement(elementName, elementGuid, tags, false, attached);
}
public override void ExecuteBM()
{
FastNoteTracker.GenerateElement(elementName, elementGuid, tags, false, GetElement(attachedElementGuid));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7cd635f67bf63c4bbbb6b3ff8c1a295
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,363 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DG.Tweening;
using Ichni;
using Ichni.RhythmGame;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public partial class EventPoint : MonoBehaviour
{
public AnimatedFloat animatedFloat;
public EventPoint LastEventPoint;
public EventPoint NextEventPoint;
public Image EvDrawimage;
public Image OvDrawimage;
public RectTransform LeftSide;
public RectTransform RightSide;
public Button selectButton;
public RawImage CurveCanvas;
public FlexibleFloatTab FatherTab;
public TMP_Text ViewText;
public static bool Locked = false;
public int BeatDeviver => FatherTab.BeatDeviver;
public void Initialize(AnimatedFloat animatedFloat)
{
this.animatedFloat = animatedFloat;
transform.localPosition = new Vector3(
animatedFloat.startTime / EditorManager.instance.timeline.timePerBeat * BeatDeviver, 0, 0
);
RightSide.localPosition = new Vector3(
(animatedFloat.endTime - animatedFloat.startTime) / EditorManager.instance.timeline.timePerBeat * BeatDeviver, 0, 0);
EvDrawimage.rectTransform.sizeDelta = new Vector2(RightSide.localPosition.x - LeftSide.localPosition.x, EvDrawimage.rectTransform.sizeDelta.y);
EvDrawimage.transform.localPosition = new Vector3(EvDrawimage.rectTransform.sizeDelta.x / 2, 0, 0);
OvDrawimage.transform.localPosition = RightSide.localPosition;
CurveCanvas.rectTransform.sizeDelta = new Vector2(EvDrawimage.rectTransform.sizeDelta.x, EvDrawimage.rectTransform.sizeDelta.y);
}
public float value => FatherTab.scalevalue;
public void Refresh(bool changePos = false)
{
if (changePos)
{
Initialize(animatedFloat);
}
UpdateValue();
ReDraw(value);
}
public void SelectButtonClick()//unity内当按钮按下时
{
if (Locked) return;
if (Keyboard.current.leftShiftKey.isPressed)
{
if (FatherTab.FatherWindow.ClipBoard[FatherTab.Title].Contains(animatedFloat))
{
FatherTab.FatherWindow.ClipBoard[FatherTab.Title].Remove(animatedFloat);
LeftSide.sizeDelta = new Vector2(15, EvDrawimage.rectTransform.sizeDelta.y);
}
else
{
FatherTab.FatherWindow.ClipBoard[FatherTab.Title].Add(animatedFloat);
LeftSide.sizeDelta = EvDrawimage.rectTransform.sizeDelta;
}
FatherTab.FatherWindow.updateClipBoardMuM();
}
else UpLoad();
}
public void UpLoad()
{
FatherTab.FatherWindow.animationCurveTypeDropdown.onValueChanged.RemoveAllListeners();
// 如果当前点是已连接点,则取消连接并隐藏可见区域
if (FatherTab.FatherWindow.ConnectedPoint == this)
{
FatherTab.FatherWindow.VisibleArea.SetActive(false);
FatherTab.FatherWindow.ConnectedPoint = null;
EvDrawimage.color = new Color(EvDrawimage.color.r, 0.3019607843137255f, EvDrawimage.color.b, 0.5f);
FatherTab.FatherWindow.StartText.text = "";
FatherTab.FatherWindow.EndText.text = "";
FatherTab.FatherWindow.StartValueText.text = "";
FatherTab.FatherWindow.EndValueText.text = "";
return;
}
// 如果有已连接点,则重置其颜色
if (FatherTab.FatherWindow.ConnectedPoint != null)
{
FatherTab.FatherWindow.EvEndpointChangeButton.GetComponent<Image>().color = new Color(1f, 1f, 1f, 1);
FatherTab.FatherWindow.ConnectedPoint.EvDrawimage.color = new Color(
FatherTab.FatherWindow.ConnectedPoint.EvDrawimage.color.r,
0.3019607843137255f,
FatherTab.FatherWindow.ConnectedPoint.EvDrawimage.color.b, 0.5f
);
}
UpdateValue();
}
public void UpdateValue()
{
// 设置新的连接点并更新UI
FatherTab.FatherWindow.ConnectedPoint = this;
EvDrawimage.color = new Color(EvDrawimage.color.r, 0.75f, EvDrawimage.color.b, 1f);
FatherTab.FatherWindow.animationCurveTypeDropdown.onValueChanged.RemoveAllListeners();
// 更新下拉选项
FatherTab.FatherWindow.animationCurveTypeDropdown.ClearOptions();
List<string> enumNameList = System.Enum.GetNames(typeof(AnimationCurveType)).ToList();
FatherTab.FatherWindow.VisibleArea.SetActive(true);
FatherTab.FatherWindow.animationCurveTypeDropdown.AddOptions(enumNameList);
FatherTab.FatherWindow.animationCurveTypeDropdown.value = (int)animatedFloat.animationCurveType;
// 更新文本
FatherTab.FatherWindow.StartText.text = animatedFloat.startTime.ToString();
FatherTab.FatherWindow.EndText.text = animatedFloat.endTime.ToString();
FatherTab.FatherWindow.StartValueText.text = animatedFloat.startValue.ToString();
FatherTab.FatherWindow.EndValueText.text = animatedFloat.endValue.ToString();
FatherTab.FatherWindow.animationCurveTypeDropdown.onValueChanged.AddListener(value => FatherTab.FatherWindow.ChangeValue());
}
// 添加静态方法:查找插入索引
public static int FindInsertIndex(List<EventPoint> eventPoints, float startTime)
{
int low = 0;
int high = eventPoints.Count - 1;
while (low <= high)
{
int mid = (low + high) / 2;
if (eventPoints[mid].animatedFloat.startTime < startTime)
{
low = mid + 1;
}
else
{
high = mid - 1;
}
}
return low;
}
// 添加实例方法:连接事件点
public void LinkEventPoints(List<EventPoint> eventPoints, int index)
{
if (index - 1 >= 0)
{
LastEventPoint = eventPoints[index - 1];
LastEventPoint.NextEventPoint = this;
}
else
{
LastEventPoint = null;
}
if (index == eventPoints.Count - 1)
{
NextEventPoint = null;
}
}
// 添加实例方法:连接新事件点
public void LinkNewEventPoint(List<EventPoint> eventPoints, bool link, float scalevalue)
{
int index = eventPoints.IndexOf(this);
if (index - 1 >= 0)
{
LastEventPoint = eventPoints[index - 1];
LastEventPoint.NextEventPoint = this;
LastEventPoint.ReDraw(scalevalue);
}
if (index + 1 < eventPoints.Count)
{
NextEventPoint = eventPoints[index + 1];
if (link) animatedFloat.endTime = NextEventPoint.animatedFloat.startTime;
Initialize(animatedFloat);
NextEventPoint.LastEventPoint = this;
}
}
// 添加静态方法:克隆 AnimatedFloat 并应用时间偏移
/// <summary>
/// 克隆一个 AnimatedFloat 对象,并根据偏移量调整其开始和结束时间。
/// </summary>
/// <param name="animatedFloat">要克隆的 AnimatedFloat 对象。</param>
/// <param name="offset">时间偏移量。</param>
/// <returns>克隆后的 AnimatedFloat 对象。</returns>
public static AnimatedFloat CloneWithOffset(AnimatedFloat animatedFloat, float offset)
{
return new AnimatedFloat(
animatedFloat.startTime + offset,
animatedFloat.endTime + offset,
animatedFloat.startValue,
animatedFloat.endValue,
animatedFloat.animationCurveType
);
}
}
public partial class EventPoint//显示?
{
public IEnumerator GenerateTextureCoroutine(int width, int height, float value)
{
Task<Color[]> task = Task.Run(() => GenerateTextureColors(width, height, value));
while (!task.IsCompleted)
{
yield return null; // 等待下一帧
}
Color[] textureColors = task.Result;
Texture2D Texture = new Texture2D(width, height);
Texture.SetPixels(textureColors);
Texture.Apply();
CurveCanvas.texture = Texture;
// CurveCanvas.color = new Color(1, 1, 1, 0);
// CurveCanvas.DOColor(new Color(1, 1, 1, 1), 0.2f).SetEase(Ease.InOutSine);
}
public Color[] GenerateTextureColors(int width, int height, float value)
{
Color[] pixels = new Color[width * height];
// 初始化所有像素为透明
for (int i = 0; i < pixels.Length; i++)
{
pixels[i] = new Color(0, 0, 0, 0);
}
int LastEventPointY = 0;
for (int i = 0; i < width; i++)
{
float t = (float)i / width;
int f = (int)(
(height / 2) + (animatedFloat.startValue * value + ((animatedFloat.endValue - animatedFloat.startValue)
* AnimationCurveEvaluator.Evaluate(animatedFloat.animationCurveType, t) * value))
);
// 绘制垂直线段 - 保留超出边界的红色标记
if (LastEventPointY < f)
{
for (int j = LastEventPointY; j < f; j++)
{
// 检查是否超出边界
bool isOutOfBounds = j < 0 || j >= height;
// 计算实际坐标(循环调整)
int actualY = j;
while (actualY < 0) actualY += height;
while (actualY >= height) actualY -= height;
int index = actualY * width + i;
if (index >= 0 && index < pixels.Length)
{
// 根据是否超出边界设置颜色
pixels[index] = isOutOfBounds ? Color.red : Color.green;
}
}
}
else
{
for (int j = LastEventPointY; j > f; j--)
{
// 检查是否超出边界
bool isOutOfBounds = j < 0 || j >= height;
// 计算实际坐标(循环调整)
int actualY = j;
while (actualY < 0) actualY += height;
while (actualY >= height) actualY -= height;
int index = actualY * width + i;
if (index >= 0 && index < pixels.Length)
{
// 根据是否超出边界设置颜色
pixels[index] = isOutOfBounds ? Color.red : Color.green;
}
}
}
// 绘制当前点 - 保留超出边界的红色标记
bool isFOutOfBounds = f < 0 || f >= height;
int actualF = f;
while (actualF < 0) actualF += height;
while (actualF >= height) actualF -= height;
int currentIndex = actualF * width + i;
if (currentIndex >= 0 && currentIndex < pixels.Length)
{
// 根据是否超出边界设置颜色
pixels[currentIndex] = isFOutOfBounds ? Color.red : Color.green;
}
LastEventPointY = f;
}
return pixels;
}
public void ReDraw(float value)
{
int width = (int)CurveCanvas.rectTransform.sizeDelta.x / 5;
int height = (int)CurveCanvas.rectTransform.sizeDelta.y / 5;
// 获取颜色数组(可在多线程环境中调用)
// 在主线程中创建和设置纹理Unity对象操作必须在主线程
StartCoroutine(GenerateTextureCoroutine(width, height, value));
// 其余的非纹理相关代码保持不变
if (NextEventPoint != null)
{
OvDrawimage.transform.localPosition = new Vector3(RightSide.transform.localPosition.x,
animatedFloat.endValue * value * 5, 0);
OvDrawimage.rectTransform.sizeDelta = new Vector2((NextEventPoint.animatedFloat.startTime - animatedFloat.endTime) / EditorManager.instance.uiManager.timeline.timePerBeat * FatherTab.BeatDeviver,
OvDrawimage.rectTransform.sizeDelta.y);
OvDrawimage.color = new Color(0, 1, 0, 1);
while (OvDrawimage.transform.localPosition.y > 100)
{
OvDrawimage.color = new Color(1, 0, 0, 0.3f);
OvDrawimage.transform.localPosition = new Vector3(OvDrawimage.transform.localPosition.x, OvDrawimage.transform.localPosition.y - 200, OvDrawimage.transform.localPosition.z);
}
while (OvDrawimage.transform.localPosition.y < -100)
{
OvDrawimage.color = new Color(1, 0, 0, 0.3f);
OvDrawimage.transform.localPosition = new Vector3(OvDrawimage.transform.localPosition.x, OvDrawimage.transform.localPosition.y + 200, OvDrawimage.transform.localPosition.z);
}
}
else
{
OvDrawimage.rectTransform.sizeDelta = new Vector2(0, OvDrawimage.rectTransform.sizeDelta.y);
}
selectButton.transform.localPosition = EvDrawimage.transform.localPosition;
selectButton.GetComponent<RectTransform>().sizeDelta = EvDrawimage.rectTransform.sizeDelta;
ViewText.text = animatedFloat.startTime.ToString("0.00") + "s" + "\n" +
animatedFloat.startValue.ToString("0.0") + "\n" + animatedFloat.endValue.ToString("0.0") + "\n" + animatedFloat.endTime.ToString("0.00") + "s" + "\n" +
animatedFloat.animationCurveType.ToString();
ViewText.color = new Color(1, 1, 1, EvDrawimage.rectTransform.sizeDelta.x < 100 ? 0 : 1);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b4c5757a536b2e14bbb3c3fa2ed0a8e4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,333 @@
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using DG.Tweening.Core.Easing;
using Ichni;
using Ichni.Editor;
using Ichni.RhythmGame;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public class FlexibleFloatTab : MonoBehaviour
{
public GraphicalFlexibleFloatWindow FatherWindow;
public List<EventPoint> eventPoints;
public RectTransform Area;
public RectTransform BeatArea;
public RectTransform XBeatArea;
public EventPoint eventPoint;
public GameObject BeatLine;
public Button TabButton;
public string Title;
public FlexibleFloat connectFloat;
public int BeatDeviver => FatherWindow.BeatDeviver;
public int BeatNextDeviver => FatherWindow.BeatNextDeviver;
public bool linked = false;//TODOTODO!
// 初始化函数
public void Initialize(FlexibleFloat flexibleFloat, string title)
{
Title = title;
ClearChildren(Area);
ClearChildren(BeatArea);
eventPoints = new List<EventPoint>();
connectFloat = flexibleFloat;
CreateBeatLines();
CreateEventPoints();
Area.localPosition = new Vector3(FatherWindow.songBeat * BeatDeviver, 0, 0);
}
// 清除子节点
private void ClearChildren(RectTransform parent)
{
for (int i = 0; i < parent.childCount; i++)
{
Destroy(parent.GetChild(i).gameObject);
}
}
// 创建节拍线
private void CreateBeatLines()
{
// 先清空BeatArea下的所有子对象防止重复生成
for (int i = BeatArea.childCount - 1; i >= 0; i--)
{
Destroy(BeatArea.GetChild(i).gameObject);
}
float maxX = 1400f + (3 * BeatDeviver);
int totalBeats = (int)(EditorManager.instance.songInformation.song.length / FatherWindow.timePerBeat);
for (int i = 0; i < totalBeats; i++)
{
float posX = BeatDeviver * i;
if (posX > maxX)
{
break;
}
GameObject u = Instantiate(BeatLine, BeatArea);
u.transform.localPosition = new Vector3(posX, 0, 0);
}
}
// 创建事件点
private void CreateEventPoints()
{
for (int i = 0; i <= connectFloat.animations.Count - 1; i++)
{
AnimatedFloat animatedFloat = connectFloat.animations[i];
EventPoint eventPoint = Instantiate(this.eventPoint, Area);
eventPoint.FatherTab = this;
eventPoint.Initialize(animatedFloat);
eventPoints.Add(eventPoint);
LinkEventPoints(i, eventPoint);
}
foreach (var i in eventPoints)
{
i.ReDraw(scalevalue);
}
}
// 更新函数
public void Update()
{
bool isOnEv = false;
foreach (var i in eventPoints)
{
isOnEv = RectTransformUtility.RectangleContainsScreenPoint(i.selectButton.GetComponent<RectTransform>(), Mouse.current.position.ReadValue());
if (isOnEv) break;
}
Vector3 newPosition = new Vector3(-FatherWindow.songBeat * BeatDeviver, 0, 0);
Area.localPosition = newPosition;
BeatArea.localPosition = newPosition;
while (true)
{
BeatArea.localPosition += new Vector3(BeatDeviver, 0, 0);
if (BeatArea.localPosition.x > ((-200f) - BeatDeviver))
{
break;
}
}
XBeatArea.localPosition = BeatArea.localPosition;
if (RectTransformUtility.RectangleContainsScreenPoint(TabButton.GetComponent<RectTransform>(), Mouse.current.position.ReadValue()))
if (!isOnEv && Mouse.current.leftButton.wasPressedThisFrame)
{
AddEvent();
}
}
// 添加事件
public void AddEvent()
{
if (Keyboard.current.ctrlKey.isPressed)
{
EventPoint eventPoint = Instantiate(this.eventPoint, Area);
eventPoint.FatherTab = this;
eventPoint.Initialize(new AnimatedFloat(GetBeat(),
GetBeat() + (float.Parse(FatherWindow.EventMultiplier.text) * FatherWindow.timePerBeat), 0, 0, AnimationCurveType.Linear));
eventPoints.Insert(FindInsertIndex(eventPoint.animatedFloat.startTime), eventPoint);
LinkNewEventPoint(eventPoint, linked);
SetEventPoint(eventPoint);
eventPoint.ReDraw(scalevalue);
eventPoint.selectButton.onClick.Invoke();
FatherWindow.ChangeValue();
connectFloat.Add(eventPoint.animatedFloat);
connectFloat.Sort();
}
// else
// {
// if (FatherWindow.ConnectedPoint != null) FatherWindow.ConnectedPoint.UpLoad();
// }
}
public void SpawnEvent(AnimatedFloat animatedFloat)
{
EventPoint eventPoint = Instantiate(this.eventPoint, Area);
eventPoint.FatherTab = this;
eventPoint.Initialize(animatedFloat);
eventPoints.Insert(FindInsertIndex(eventPoint.animatedFloat.startTime), eventPoint);
LinkNewEventPoint(eventPoint);
eventPoint.ReDraw(scalevalue);
connectFloat.Add(eventPoint.animatedFloat);
connectFloat.Sort();
}
public void SetEventPoint(EventPoint eventPoint)
{
if (eventPoint.LastEventPoint == null) return;
// 设置当前事件点的起始值为上一个事件点的结束值
eventPoint.animatedFloat.startValue = eventPoint.LastEventPoint.animatedFloat.endValue;
eventPoint.animatedFloat.animationCurveType = eventPoint.LastEventPoint.animatedFloat.animationCurveType;
eventPoint.ReDraw(scalevalue);
}
// 添加调用 EventPoint 类的接口
public void LinkEventPoints(int index, EventPoint eventPoint)
{
eventPoint.LinkEventPoints(eventPoints, index);
}
public void LinkNewEventPoint(EventPoint eventPoint, bool link = false)
{
eventPoint.LinkNewEventPoint(eventPoints, link, scalevalue);
}
public int FindInsertIndex(float startTime)
{
return EventPoint.FindInsertIndex(eventPoints, startTime);
}
// 获取节拍
public float GetBeat()
{
// 获取鼠标在 BeatArea 中的相对位置
Vector2 localMousePosition = Area.InverseTransformPoint(Mouse.current.position.ReadValue());
//Debug.Log(localMousePosition);
float mouseBeat = localMousePosition.x / BeatDeviver;
float far = 0f;
while (far < mouseBeat)
{
far += 1f / BeatNextDeviver;
}
far -= 1f / BeatNextDeviver;
return far * FatherWindow.timePerBeat;
}
public float scalevalue => FatherWindow.scalevalue;
// 曲线缩放
public void CurveScale(float value)
{
foreach (EventPoint i in eventPoints)
{
i.ReDraw(value);
}
}
// 改变X轴节拍
public void XbeatCnange(int num)
{
ClearChildren(XBeatArea);
for (int i = 1; i < num; i++)
{
foreach (Transform child in BeatArea)
{
GameObject newChild = Instantiate(child.gameObject, XBeatArea);
CanvasGroup canvasGroup = newChild.GetComponent<CanvasGroup>();
newChild.transform.localPosition = new Vector3(child.localPosition.x + ((float)BeatDeviver) / num * i, child.localPosition.y, child.localPosition.z);
if (canvasGroup == null)
{
canvasGroup = newChild.AddComponent<CanvasGroup>();
}
canvasGroup.alpha = 0.1f;
}
}
}
// 移除动画
public void remoceAnim(AnimatedFloat a)
{
if (connectFloat.animations.Contains(a))
{
connectFloat.animations.Remove(a);
}
}
/// <summary>
/// 从事件点列表中移除指定的事件点,并更新其前后连接关系。
/// </summary>
/// <param name="eventPoint">要移除的事件点。</param>
public void RemoveEventPoint(EventPoint eventPoint)
{
if (eventPoints.Contains(eventPoint))
{
// 更新前后事件点的连接关系
if (eventPoint.LastEventPoint != null)
{
eventPoint.LastEventPoint.NextEventPoint = eventPoint.NextEventPoint;
eventPoint.LastEventPoint.ReDraw(scalevalue);
}
if (eventPoint.NextEventPoint != null)
{
eventPoint.NextEventPoint.LastEventPoint = eventPoint.LastEventPoint;
eventPoint.NextEventPoint.ReDraw(scalevalue);
}
// 从列表中移除事件点
eventPoints.Remove(eventPoint);
// 从连接的动画中移除
connectFloat.animations.Remove(eventPoint.animatedFloat);
// 销毁事件点对象
Destroy(eventPoint.gameObject);
}
}
public IEnumerator Draging()
{
EventPoint.Locked = true;
while (Keyboard.current.uKey.isPressed)
{
bool changed = false;
var point = FatherWindow.ConnectedPoint;
if (Mouse.current.leftButton.isPressed)
{
point.animatedFloat.startTime = GetBeat();
changed = true;
}
else if (Mouse.current.rightButton.isPressed)
{
point.animatedFloat.endTime = GetBeat();
changed = true;
}
if (changed)
{
// 保证时间顺序
if (point.animatedFloat.startTime >= point.animatedFloat.endTime)
{
if (Mouse.current.leftButton.isPressed)
point.animatedFloat.endTime = point.animatedFloat.startTime + 0.1f;
else
point.animatedFloat.startTime = point.animatedFloat.endTime - 0.1f;
}
// 保证不超过下一个事件点
if (point.NextEventPoint != null && point.animatedFloat.endTime > point.NextEventPoint.animatedFloat.startTime)
{
if (Mouse.current.leftButton.isPressed)
point.animatedFloat.endTime = point.NextEventPoint.animatedFloat.startTime;
else
point.animatedFloat.startTime = point.NextEventPoint.animatedFloat.startTime - 0.1f;
}
point.Initialize(point.animatedFloat);
point.UpdateValue();
}
yield return null;
}
FatherWindow.ConnectedPoint.Refresh(true);
EventPoint.Locked = false;
}
public void CutEvent()
{
EventPoint eventPoint = FatherWindow.ConnectedPoint;
if (eventPoint == null) return;
for (int i = 0; i < BeatNextDeviver; i++)
{
float segmentStartTime = eventPoint.animatedFloat.startTime + (eventPoint.animatedFloat.endTime - eventPoint.animatedFloat.startTime) / BeatNextDeviver * i;
float segmentEndTime = eventPoint.animatedFloat.startTime + (eventPoint.animatedFloat.endTime - eventPoint.animatedFloat.startTime) / BeatNextDeviver * (i + 1);
connectFloat.Add(new AnimatedFloat(
segmentStartTime,
segmentEndTime,
AnimationCurveEvaluator.Evaluate(eventPoint.animatedFloat.animationCurveType, (segmentStartTime - eventPoint.animatedFloat.startTime) / (eventPoint.animatedFloat.endTime - eventPoint.animatedFloat.startTime)) * (eventPoint.animatedFloat.endValue - eventPoint.animatedFloat.startValue) + eventPoint.animatedFloat.startValue,
AnimationCurveEvaluator.Evaluate(eventPoint.animatedFloat.animationCurveType, (segmentEndTime - eventPoint.animatedFloat.startTime) / (eventPoint.animatedFloat.endTime - eventPoint.animatedFloat.startTime)) * (eventPoint.animatedFloat.endValue - eventPoint.animatedFloat.startValue) + eventPoint.animatedFloat.startValue,
AnimationCurveType.Linear));
}
RemoveEventPoint(eventPoint);
connectFloat.Sort();
Initialize(connectFloat, Title);
FatherWindow.ChangeValue();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 20904e6182baed446b49fe294fad22b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,309 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
using Ichni;
using Ichni.Editor;
using Ichni.RhythmGame;
using Sirenix.OdinInspector;
using Sirenix.Utilities;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public partial class GraphicalFlexibleFloatWindow : MovableWindow
{
public FlexibleFloatTab unitPrefab;
public IBaseElement connectedBaseElement;
public List<FlexibleFloatTab> unitList;
public UnityAction ApplyParameters;
public float songTime => EditorManager.instance.songInformation.songTime;
public float songBeat => EditorManager.instance.songInformation.songBeat;
public float beatmapStartTime => -EditorManager.instance.songInformation.delay;
public float timePerBeat => 60f / EditorManager.instance.songInformation.bpm;
public int BeatDeviver = 100;
public int BeatNextDeviver = 1;
public void Initialize(IBaseElement baseElement, string title, FlexibleFloat[] FlexibleFloats, string[] Subtitles)
{
InitializeWindow(title, null, new Vector3(1f, 1f, 1f));
scalevalue = 5;
this.connectedBaseElement = baseElement;
this.title.text = title;
unitList = new List<FlexibleFloatTab>();
closeButton.onClick.AddListener(Quit);
for (int i = 0; i <= FlexibleFloats.Length - 1; i++)
{
ClipBoard.Add(Subtitles[i], new List<AnimatedFloat>());
AddUnit(FlexibleFloats[i], Subtitles[i]);
}
ApplyParameters = () =>
{
for (int i = 0; i <= unitList.Count - 1; i++)
{
unitList[i].connectFloat.Sort();
}
};
XDeviverScale("1");
EvEndpointChangeButton.onClick.AddListener(EvEndpointStartChange);
GetComponent<RectTransform>().sizeDelta = new Vector2(1400, (FlexibleFloats.Length + 1) * 200 + 50);
}
public void AddUnit(FlexibleFloat flexibleFloat, string Subtitle)
{
flexibleFloat.Sort();
FlexibleFloatTab flexibleFloatTab = Instantiate(unitPrefab, windowRect);
flexibleFloatTab.FatherWindow = this;
unitList.Add(flexibleFloatTab);
flexibleFloatTab.Initialize(flexibleFloat, Subtitle);
}
public void Quit()
{
ApplyParameters();
//StartCoroutine(WindowAnim.HidePanel(gameObject, true));
this.transform.DOScale(Vector3.zero, 0.15f).SetEase(Ease.InCirc).OnComplete(() =>
{
Destroy(gameObject);
});
}
public float scalevalue;
public void CurveScale(string Rawvalue)
{
float value = float.Parse(Rawvalue);
scalevalue = value;
foreach (FlexibleFloatTab i in unitList)
{
i.CurveScale(value);
}
}
public void DeviverScale(string Rawvalue)
{
if (ConnectedPoint != null) ConnectedPoint.UpLoad();
BeatDeviver = int.Parse(Rawvalue);
ChangeValue();
for (int i = unitList.Count - 1; i >= 0; i--)
{
unitList[i].Initialize(unitList[i].connectFloat, unitList[i].Title);
}
XDeviverScale(BeatNextDeviver.ToString());
}
public void XDeviverScale(string Rawvalue)
{
BeatNextDeviver = int.Parse(Rawvalue);
foreach (FlexibleFloatTab i in unitList)
{
i.XbeatCnange(BeatNextDeviver);
}
}
}
public partial class GraphicalFlexibleFloatWindow
{
[Title("AnimEditor")]
public TMP_InputField StartText;
public TMP_InputField EndText;
public TMP_InputField StartValueText;
public TMP_InputField EndValueText;
public TMP_InputField EventMultiplier;
public EventPoint ConnectedPoint;
public TMP_Dropdown animationCurveTypeDropdown;
public GameObject VisibleArea;
/// <summary>
/// 移除当前连接的事件点,并更新 UI。
/// </summary>
public void RemoveConnectedPoint()
{
if (ConnectedPoint != null)
{
// 调用 FlexibleFloatTab 的 RemoveEventPoint 方法
ConnectedPoint.FatherTab.RemoveEventPoint(ConnectedPoint);
// 清空连接点并隐藏可见区域
ConnectedPoint = null;
VisibleArea.SetActive(false);
}
}
public void ChangeValue()
{
if (ConnectedPoint != null)
{
float startTime = float.Parse(StartText.text);
float endTime = float.Parse(EndText.text);
float startValue = float.Parse(StartValueText.text);
float endValue = float.Parse(EndValueText.text);
ConnectedPoint.animatedFloat.startTime = startTime;
ConnectedPoint.animatedFloat.endTime = endTime;
ConnectedPoint.animatedFloat.startValue = startValue;
ConnectedPoint.animatedFloat.endValue = endValue;
ConnectedPoint.animatedFloat.animationCurveType = (AnimationCurveType)animationCurveTypeDropdown.value;
ConnectedPoint.Initialize(ConnectedPoint.animatedFloat);
ConnectedPoint.ReDraw(scalevalue);
}
}
public Button EvEndpointChangeButton;
public void EvEndpointStartChange()
{
if (ConnectedPoint != null)
{
EndText.text = (ConnectedPoint.animatedFloat.startTime + 0.01).ToString();
ChangeValue();
EvEndpointChangeButton.GetComponent<Image>().color = new Color(1f, 0.5f, 0.5f, 1);
ConnectedPoint.FatherTab.TabButton.onClick.RemoveAllListeners();
ConnectedPoint.FatherTab.TabButton.onClick.AddListener(EvEndpointEndChange);
}
}
public void EvEndpointEndChange()
{
if (ConnectedPoint != null)
{
EvEndpointChangeButton.GetComponent<Image>().color = new Color(1f, 1f, 1f, 1);
float newendtime = ConnectedPoint.FatherTab.GetBeat();
if (newendtime > ConnectedPoint.animatedFloat.startTime)
{
if (ConnectedPoint.NextEventPoint != null && newendtime > ConnectedPoint.NextEventPoint.animatedFloat.startTime)
EndText.text = ConnectedPoint.NextEventPoint.animatedFloat.startTime.ToString();
else EndText.text = newendtime.ToString();
ChangeValue();
}
ConnectedPoint.FatherTab.TabButton.onClick.RemoveAllListeners();
}
}
public Dictionary<string, List<AnimatedFloat>> ClipBoard = new();
public void Update()
{
if (Keyboard.current.deleteKey.isPressed && ConnectedPoint != null)
{
RemoveConnectedPoint();
}
if (Keyboard.current.shiftKey.isPressed)
{
if (Keyboard.current.vKey.wasPressedThisFrame)
PasteClipboard(1f);
else if (Keyboard.current.bKey.wasPressedThisFrame)
PasteClipboard(-1f);
}
if (Keyboard.current.escapeKey.wasPressedThisFrame)
{
foreach (var key in ClipBoard.Keys.ToList())
{
ClipBoard[key] = new List<AnimatedFloat>();
}
foreach (FlexibleFloatTab i in unitList)
{
foreach (EventPoint j in i.eventPoints)
{
j.LeftSide.sizeDelta = new Vector2(15, j.EvDrawimage.rectTransform.sizeDelta.y);
}
}
updateClipBoardMuM();
}
if (ConnectedPoint != null && RectTransformUtility.RectangleContainsScreenPoint(GetComponent<RectTransform>(), Mouse.current.position.ReadValue()))
{
if (Keyboard.current.aKey.wasPressedThisFrame)
{
ConnectedPoint.animatedFloat.endValue *= -1;
ConnectedPoint.animatedFloat.startValue *= -1;
ConnectedPoint.Refresh();
}
else if (Keyboard.current.sKey.wasPressedThisFrame)
{
float value = ConnectedPoint.animatedFloat.startValue;
ConnectedPoint.animatedFloat.startValue = ConnectedPoint.animatedFloat.endValue;
ConnectedPoint.animatedFloat.endValue = value;
ConnectedPoint.Refresh();
}
else if (Keyboard.current.uKey.wasPressedThisFrame)
{
try
{
ConnectedPoint.FatherTab.StartCoroutine(ConnectedPoint.FatherTab.Draging());
}
catch (System.Exception)
{
EventPoint.Locked = false;
}
}
else if (Keyboard.current.lKey.wasPressedThisFrame)
{
ConnectedPoint.FatherTab.CutEvent();
}
}
}
public void PasteClipboard(float valuescale = 1f)
{
// 获取当前时间线的节拍位置
float time = unitList[0].GetBeat();
float MinCopyTime = float.MaxValue;
// 遍历剪贴板中的所有动画数据MinCopyTime = float.MaxValue;
foreach (var list in ClipBoard.Values)
{
foreach (var animatedFloat in list)
{
if (animatedFloat.startTime < MinCopyTime)
{
MinCopyTime = animatedFloat.startTime;
}
}
}
foreach (var key in ClipBoard.Keys)
{
foreach (var animatedFloat in ClipBoard[key])
{
// 克隆动画数据并应用时间偏移
AnimatedFloat newFloat = EventPoint.CloneWithOffset(animatedFloat, time - MinCopyTime);
newFloat.startValue *= valuescale;
newFloat.endValue *= valuescale;
// 在对应的 FlexibleFloatTab 中生成事件点
unitList.Find(x => x.Title == key).SpawnEvent(newFloat);
}
}
}
}
public partial class GraphicalFlexibleFloatWindow
{//以后显示类写这里,别在叠大粪了
public TMP_Text ClipBoardMuM;
public void updateClipBoardMuM()
{
int mum = 0;
foreach (var key in ClipBoard.Keys)
{
mum += ClipBoard[key].Count();
}
ClipBoardMuM.text = "ClipBoard: " + mum.ToString();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f9ccff815b775ec48abb8e62577257f8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,349 @@
using System.Collections;
using System.Collections.Generic;
using Ichni;
using Ichni.Editor;
using Ichni.RhythmGame;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public partial class HsvDrawer
{
[Header("Color Wheel Settings")]
public int textureSize = 256; // 纹理尺寸
public float saturation = 1f; // 最大饱和度
public float value = 1f; // 明度
[Header("Color Picker Settings")]
public RectTransform pickerIndicator; // 取色器指示器
public Color currentColor = Color.white; // 当前选中的颜色
public Slider valueSlider; // 亮度拖动条
public Slider alphaSlider; // 透明度拖动条
public RawImage previewImage; // 颜色预览窗
private Texture2D _texture;
public RawImage _rawImage;
private RectTransform _rectTransform;
public bool isEmission = false;
public GameObject emissionObj;
public InputField emissionInput;
public DynamicUIBaseColorPicker baseColorPicker;
void Start()
{
_rectTransform = _rawImage.GetComponent<RectTransform>(); // 初始化
CreateColorWheelTexture();
ApplyTextureToRawImage();
if (valueSlider != null)
{
valueSlider.minValue = 0f;
valueSlider.maxValue = 1f;
valueSlider.value = value;
valueSlider.onValueChanged.AddListener(OnValueSliderChanged);
}
if (alphaSlider != null)
{
alphaSlider.minValue = 0f;
alphaSlider.maxValue = 1f;
alphaSlider.onValueChanged.AddListener(OnAlphaSliderChanged);
}
if (previewImage != null)
{
previewImage.color = currentColor;
}
}
private Coroutine valueSliderCoroutine;
private float pendingValue = -1f;
// 新增:高低分辨率参数
public int highResTextureSize = 256;
public int lowResTextureSize = 64;
private bool isLowRes = false;
void OnValueSliderChanged(float newValue)
{
value = newValue;
pendingValue = newValue;
// 切换到低分辨率
if (!isLowRes)
{
isLowRes = true;
CreateColorWheelTexture(true);
ApplyTextureToRawImage();
}
if (valueSliderCoroutine != null)
StopCoroutine(valueSliderCoroutine);
valueSliderCoroutine = StartCoroutine(DelayedRedrawColorWheel());
// 刷新当前颜色
if (pickerIndicator != null)
UpdateCurrentColor(pickerIndicator.localPosition);
}
IEnumerator DelayedRedrawColorWheel()
{
float waitTime = 0.01f;
float timer = 0f;
while (timer < waitTime)
{
yield return null;
timer += Time.unscaledDeltaTime;
}
// 恢复高分辨率
if (isLowRes)
{
isLowRes = false;
CreateColorWheelTexture(false);
ApplyTextureToRawImage();
}
else
{
CreateColorWheelTexture(false);
ApplyTextureToRawImage();
}
valueSliderCoroutine = null;
}
void OnAlphaSliderChanged(float newAlpha)
{
// 只更新透明度
currentColor.a = newAlpha;
if (previewImage != null)
previewImage.color = currentColor;
}
void Update()
{
// 鼠标点击色轮区域即可拖动
if (Mouse.current.leftButton.wasPressedThisFrame && RectTransformUtility.RectangleContainsScreenPoint(_rectTransform, Mouse.current.position.ReadValue()))
{
StartCoroutine(DraggingColorPicker());
}
}
IEnumerator DraggingColorPicker()
{
while (Mouse.current.leftButton.isPressed)
{
Vector2 localPosition;
RectTransformUtility.ScreenPointToLocalPointInRectangle(_rectTransform, Mouse.current.position.ReadValue(), null, out localPosition);
// 计算中心和最大半径
Vector2 center = _rectTransform.rect.center;
float maxRadius = _rectTransform.rect.width / 2f;
Vector2 offset = localPosition - center;
float distance = offset.magnitude;
// 如果超出圆形区域,则贴边
if (distance > maxRadius)
{
offset = offset.normalized * maxRadius;
localPosition = center + offset;
}
if (pickerIndicator != null)
pickerIndicator.localPosition = localPosition;
UpdateCurrentColor(localPosition);
yield return null; // 等待下一帧
}
}
// 整合分辨率逻辑到此方法
void CreateColorWheelTexture(bool useLowRes = false)
{
int targetSize = useLowRes ? lowResTextureSize : highResTextureSize;
textureSize = targetSize;
// 1. 优化:仅在尺寸变化时重新分配纹理
if (_texture == null || _texture.width != textureSize || _texture.height != textureSize)
{
_texture = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false);
_texture.filterMode = FilterMode.Bilinear; // 确保平滑
_texture.wrapMode = TextureWrapMode.Clamp;
}
// 2. 核心优化:使用 Color32 数组代替 Color 数组
// Color32 占用的内存更小SetPixels32 的执行效率远高于 SetPixels
Color32[] pixels = new Color32[textureSize * textureSize];
float halfSize = textureSize * 0.5f;
float invMaxRadius = 1.0f / halfSize; // 预计算半径倒数,将除法转为乘法
Color32 clearColor = new Color32(0, 0, 0, 0);
// 3. 循环优化
for (int y = 0; y < textureSize; y++)
{
// 预计算当前行的偏移量 Y 轴部分
float offY = (y + 0.5f) - halfSize;
float offYSq = offY * offY;
int rowOffset = y * textureSize;
for (int x = 0; x < textureSize; x++)
{
float offX = (x + 0.5f) - halfSize;
float distSq = offX * offX + offYSq; // 使用平方比较,避免在外部判断时使用 magnitude (开方)
int idx = rowOffset + x;
if (distSq <= halfSize * halfSize)
{
float distance = Mathf.Sqrt(distSq);
// 计算色相 (Hue)
float angle = Mathf.Atan2(offY, offX) * Mathf.Rad2Deg;
if (angle < 0) angle += 360f;
// 计算饱和度 (Saturation),受外部变量 saturation 影响
float sat = (distance * invMaxRadius) * saturation;
// 转换 HSV 到 RGB 并存入 Color32
// 注意Color.HSVToRGB 返回 Color赋值给 Color32 会自动隐式转换
pixels[idx] = Color.HSVToRGB(angle / 360f, Mathf.Clamp01(sat), value);
}
else
{
pixels[idx] = clearColor;
}
}
}
// 4. 应用像素数据
_texture.SetPixels32(pixels);
_texture.Apply();
}
void ApplyTextureToRawImage()
{
if (_rawImage == null)
_rawImage = GetComponent<RawImage>();
_rawImage.texture = _texture;
_rawImage.color = Color.white; // 确保显示原始颜色
}
void UpdateCurrentColor(Vector2 localPosition)
{
Vector2 center = _rectTransform.rect.center;
Vector2 offset = localPosition - center;
float maxRadius = _rectTransform.rect.width / 2f;
// 计算半径比例(饱和度)
float distance = offset.magnitude;
float sat = Mathf.Clamp01(distance / maxRadius) * saturation;
// 计算角度(色调)
float angle = Mathf.Atan2(offset.y, offset.x) * Mathf.Rad2Deg;
if (angle < 0) angle += 360;
// 计算颜色
float alpha = alphaSlider != null ? alphaSlider.value : 1f;
currentColor = Color.HSVToRGB(angle / 360f, sat, value);
currentColor.a = alpha;
// 更新取色器颜色(使其与背景有对比度)
if (pickerIndicator != null)
{
var img = pickerIndicator.GetComponent<Image>();
if (img != null)
img.color = (value > 0.5f) ? Color.black : Color.white;
}
// 若有亮度滑块,保持同步
if (valueSlider != null && valueSlider.value != value)
{
valueSlider.SetValueWithoutNotify(value);
}
// 若有透明度滑块,保持同步
if (alphaSlider != null && alphaSlider.value != alpha)
{
alphaSlider.SetValueWithoutNotify(alpha);
}
// 更新预览窗
if (previewImage != null)
{
previewImage.color = currentColor;
}
if (baseColorPicker != null)
{
// 同步到BaseColorPicker
baseColorPicker.SliderChangeWithoutNotification(currentColor);
}
ApplyParameters();
}
}
public partial class HsvDrawer : DynamicUIElement
{
private void ApplyParameters()
{
Color newValue = currentColor;
connectedBaseElement.GetType().GetField(parameterName).SetValue(connectedBaseElement, newValue);
connectedBaseElement.Refresh();
}
// 新增同步connectedBaseElement的color到色盘
public void SyncFromBaseElement()
{
if (connectedBaseElement == null || string.IsNullOrEmpty(parameterName)) return;
Color color = (Color)connectedBaseElement.GetType().GetField(parameterName).GetValue(connectedBaseElement);
currentColor = color;
// 解析HSV
Color.RGBToHSV(color, out float h, out float s, out float v);
value = v;
if (valueSlider != null)
valueSlider.SetValueWithoutNotify(v);
if (alphaSlider != null)
alphaSlider.SetValueWithoutNotify(color.a);
// 计算picker位置
if (_rectTransform == null)
_rectTransform = _rawImage.GetComponent<RectTransform>();
Vector2 center = _rectTransform.rect.center;
float maxRadius = _rectTransform.rect.width / 2f;
float angle = h * 360f * Mathf.Deg2Rad;
float radius = s * maxRadius;
Vector2 offset = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
Vector2 pickerPos = center + offset;
if (pickerIndicator != null)
pickerIndicator.localPosition = pickerPos;
// 更新色盘和预览
CreateColorWheelTexture();
ApplyTextureToRawImage();
if (previewImage != null)
previewImage.color = currentColor;
}
public override void Initialize(IBaseElement baseElement, string title, string parameterName)
{
base.Initialize(baseElement, title, parameterName);
SyncFromBaseElement();
}
public override DynamicUIElement AddListenerFunction(UnityAction action)
{
valueSlider.onValueChanged.AddListener(_ => action());
alphaSlider.onValueChanged.AddListener(_ => action());
valueSlider.onValueChanged.AddListener(_ => UpdateCurrentColor(pickerIndicator.localPosition));
alphaSlider.onValueChanged.AddListener(_ => UpdateCurrentColor(pickerIndicator.localPosition));
// 拖动色盘时也触发
// 这里通过在 UpdateCurrentColor 里调用 action
// 但由于没有事件,这里可以考虑用委托或事件,但为兼容性直接调用
// 可以在 UpdateCurrentColor 末尾加一行(如果需要多次触发):
// action?.Invoke();
// 但一般只需响应滑块即可
return this;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81a06df931b999c4e9a0ac510cb0f3ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,312 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
namespace Ichni.Editor
{
[RequireComponent(typeof(RawImage))]
public class KeyframeVisualizer : MonoBehaviour
{
[Header("Settings")]
public Vector2Int resolution = new Vector2Int(512, 256);
public Color curveColor = Color.green;
public Color gridColor = new Color(0.3f, 0.3f, 0.3f, 0.5f);
public float pointSize = 15f;
public float tangentHandleLength = 40f;
[Header("References")]
public AnimationCurve curve;
public RawImage rawImage;
// 当用户松开鼠标编辑结束时触发用于同步外部UI
public Action OnEditFinished;
private Texture2D _texture;
private Color32[] _buffer;
private List<CurvePoint> _activePoints = new List<CurvePoint>();
// 缓存特定颜色以提高性能
private Color32 _cClear = new Color32(0, 0, 0, 0);
private Color32 _cCurve;
private Color32 _cGrid;
private void Awake()
{
if (rawImage == null) rawImage = GetComponent<RawImage>();
_cCurve = curveColor;
_cGrid = gridColor;
}
private void OnEnable()
{
if (curve == null) curve = AnimationCurve.Linear(0, 0, 1, 1);
InitTexture();
RebuildInteractablePoints();
DrawCurveToRawImage();
}
// === 核心绘制逻辑 (高性能) ===
public void DrawCurveToRawImage()
{
if (curve == null || _texture == null) return;
// 1. 清屏
int len = _buffer.Length;
for (int i = 0; i < len; i++) _buffer[i] = _cClear;
int w = resolution.x;
int h = resolution.y;
// 2. 绘制网格 (0.25, 0.5, 0.75)
DrawGridLine(w, h, 0.25f);
DrawGridLine(w, h, 0.5f);
DrawGridLine(w, h, 0.75f);
// 绘制边框
DrawRect(0, 0, w - 1, h - 1, Color.white);
// 3. 绘制曲线
// 限制范围在 0-1
int prevY = -1;
for (int x = 0; x < w; x++)
{
float t = (float)x / (w - 1);
float val = curve.Evaluate(t);
// 映射到像素高度并Clap防止数组越界
int y = Mathf.FloorToInt(Mathf.Clamp01(val) * (h - 1));
// 绘制点
int idx = y * w + x;
if (idx >= 0 && idx < len) _buffer[idx] = _cCurve;
// 垂直补间(防止曲线断裂)
if (prevY != -1 && Mathf.Abs(y - prevY) > 1)
{
int step = y > prevY ? 1 : -1;
for (int k = prevY + step; k != y; k += step)
{
int fillIdx = k * w + x;
if (fillIdx >= 0 && fillIdx < len) _buffer[fillIdx] = _cCurve;
}
}
prevY = y;
}
_texture.SetPixels32(_buffer);
_texture.Apply();
rawImage.texture = _texture;
}
private void InitTexture()
{
if (_texture == null || _texture.width != resolution.x || _texture.height != resolution.y)
{
_texture = new Texture2D(resolution.x, resolution.y, TextureFormat.ARGB32, false);
_texture.filterMode = FilterMode.Bilinear;
_buffer = new Color32[resolution.x * resolution.y];
}
}
private void DrawGridLine(int w, int h, float percent)
{
int x = (int)(w * percent);
int y = (int)(h * percent);
for (int i = 0; i < h; i++) _buffer[i * w + x] = _cGrid; // 竖线
for (int i = 0; i < w; i++) _buffer[y * w + i] = _cGrid; // 横线
}
private void DrawRect(int x1, int y1, int x2, int y2, Color32 c)
{
int w = resolution.x;
for (int x = x1; x <= x2; x++) { _buffer[y1 * w + x] = c; _buffer[y2 * w + x] = c; }
for (int y = y1; y <= y2; y++) { _buffer[y * w + x1] = c; _buffer[y * w + x2] = c; }
}
// === 交互点生成 ===
public void RebuildInteractablePoints() // 原名 CreateKeyframeImages
{
if (curve == null) return;
// 清理旧点
foreach (Transform child in transform) Destroy(child.gameObject);
_activePoints.Clear();
for (int i = 0; i < curve.length; i++)
{
// 关键帧点 (Key)
var keyPoint = CreatePoint(i, PointType.Key, Color.red, pointSize);
// 入切线 (In Tangent) - 第一个点通常不需要
if (i > 0)
{
var inPoint = CreatePoint(i, PointType.InTangent, Color.cyan, pointSize * 0.6f);
inPoint.relatedKeyPoint = keyPoint;
keyPoint.inHandle = inPoint;
}
// 出切线 (Out Tangent) - 最后一个点通常不需要
if (i < curve.length - 1)
{
var outPoint = CreatePoint(i, PointType.OutTangent, Color.cyan, pointSize * 0.6f);
outPoint.relatedKeyPoint = keyPoint;
keyPoint.outHandle = outPoint;
}
}
RefreshPointsPosition();
}
private CurvePoint CreatePoint(int index, PointType type, Color color, float size)
{
GameObject go = new GameObject($"{type}_{index}");
go.transform.SetParent(transform, false);
Image img = go.AddComponent<Image>();
img.color = color;
RectTransform rt = go.GetComponent<RectTransform>();
rt.sizeDelta = Vector2.one * size;
rt.anchorMin = rt.anchorMax = Vector2.zero; // 使用绝对坐标定位
CurvePoint cp = go.AddComponent<CurvePoint>();
cp.Init(this, index, type);
_activePoints.Add(cp);
return cp;
}
// === 坐标同步逻辑 ===
public void RefreshPointsPosition()
{
Vector2 size = rawImage.rectTransform.rect.size;
float canvasAspect = size.x / size.y;
foreach (var point in _activePoints)
{
Keyframe key = curve.keys[point.keyIndex];
Vector2 keyNormPos = new Vector2(key.time, key.value);
Vector2 keyPixelPos = new Vector2(keyNormPos.x * size.x, keyNormPos.y * size.y);
if (point.type == PointType.Key)
{
point.rectTransform.anchoredPosition = keyPixelPos;
}
else
{
float tangent = (point.type == PointType.InTangent) ? key.inTangent : key.outTangent;
Vector2 visualDir;
if (float.IsInfinity(tangent))
{
visualDir = new Vector2(0, tangent > 0 ? 1 : -1);
}
else
{
// 斜率 = (y / x) * aspect => y = (tangent / aspect) * x
// 令视觉上的 x 为 1 或 -1
float xDir = (point.type == PointType.InTangent) ? -1f : 1f;
float yDir = (tangent / canvasAspect) * xDir;
visualDir = new Vector2(xDir, yDir).normalized;
}
// 顺着计算出的向量方向,放置手柄
point.rectTransform.anchoredPosition = keyPixelPos + visualDir * tangentHandleLength;
}
}
}
// 处理点被拖拽
// 在 KeyframeVisualizer 类中修改 OnPointDragged 方法
public void OnPointDragged(CurvePoint point, Vector2 screenDelta)
{
Vector2 size = rawImage.rectTransform.rect.size;
int index = point.keyIndex;
Keyframe key = curve.keys[index];
if (point.type == PointType.Key)
{
// 关键帧移动逻辑不变
Vector2 deltaNorm = new Vector2(screenDelta.x / size.x, screenDelta.y / size.y);
key.time = Mathf.Clamp01(key.time + deltaNorm.x);
key.value = Mathf.Clamp01(key.value + deltaNorm.y);
}
else
{
// === 切线处理优化:基于向量构建 ===
// 1. 获取关键帧当前的屏幕坐标
Vector2 keyPos = point.relatedKeyPoint.rectTransform.anchoredPosition;
// 2. 获取鼠标当前的目标坐标(当前手柄位置 + 增量)
Vector2 mouseTargetPos = point.rectTransform.anchoredPosition + screenDelta;
// 3. 计算从关键帧指向鼠标的向量
Vector2 dirVec = mouseTargetPos - keyPos;
// 4. 强制方向约束 (InTangent必须在左, OutTangent必须在右)
if (point.type == PointType.InTangent)
{
if (dirVec.x > -1f) dirVec.x = -1f; // 至少向左偏 1 像素,防止除零或方向翻转
}
else if (point.type == PointType.OutTangent)
{
if (dirVec.x < 1f) dirVec.x = 1f; // 至少向右偏 1 像素
}
// 5. 计算斜率 (Tangent)
// 物理斜率 = (DeltaValue / DeltaTime)
// 对应 UI = (dirVec.y / size.y) / (dirVec.x / size.x)
float canvasAspect = size.x / size.y;
float tangent = (dirVec.y / dirVec.x) * canvasAspect;
if (point.type == PointType.InTangent) key.inTangent = tangent;
else key.outTangent = tangent;
// 6. 更新手柄位置:让手柄视觉上严格对齐鼠标方向,但保持固定长度(可选)
// 如果你不希望手柄被拉长,可以将 dirVec 归一化再乘上固定长度
// point.rectTransform.anchoredPosition = keyPos + dirVec.normalized * tangentHandleLength;
}
curve.MoveKey(index, key);
DrawCurveToRawImage();
RefreshPointsPosition(); // 统一刷新位置,确保手柄视觉表现一致
}
}
public enum PointType { Key, InTangent, OutTangent }
// 辅助交互类
public class CurvePoint : MonoBehaviour, IDragHandler, IEndDragHandler
{
public KeyframeVisualizer visualizer;
public int keyIndex;
public PointType type;
public RectTransform rectTransform;
// 引用关联点,方便计算
public CurvePoint relatedKeyPoint;
public CurvePoint inHandle;
public CurvePoint outHandle;
public void Init(KeyframeVisualizer v, int index, PointType t)
{
visualizer = v;
keyIndex = index;
type = t;
rectTransform = GetComponent<RectTransform>();
}
public void OnDrag(PointerEventData eventData)
{
// 实时更新曲线和画面
visualizer.OnPointDragged(this, eventData.delta);
}
public void OnEndDrag(PointerEventData eventData)
{
// 拖拽结束后,通知外部同步 (关键一步)
visualizer.OnEditFinished?.Invoke();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e4743327f32eb24e86090ec474ac91a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,62 @@
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Ichni;
using Ichni.Editor;
using Ichni.RhythmGame;
using Unity.VisualScripting;
using UnityEngine;
public class PanelDrawer//暂时支持xz
{
public bool isEditing = false;
public CameraManager cameraManager => EditorManager.instance.cameraManager;
public SceneCamera sceneCamera => cameraManager.sceneCamera;
public float height
{
get
{
return _height;
}
set
{
_height = value;
}
}
private float _height = 10f;
public float baseHeight
{
get
{
return _baseHeight;
}
set
{
_baseHeight = value;
}
}
private float _baseHeight = 0f;
public void startEdit()
{
isEditing = true;
if (!cameraManager.isSceneCameraActive)
{
cameraManager.sceneCamera.transform.position = cameraManager.gameCamera.transform.position;
cameraManager.sceneCamera.transform.rotation = cameraManager.gameCamera.transform.rotation;
cameraManager.SwitchCamera();
cameraManager.sceneCamera.transform.DOMove(cameraManager.gameCamera.transform.position + new Vector3(0, 0, _baseHeight + _height), 0.5f).SetEase(Ease.InOutQuad);
cameraManager.sceneCamera.transform.DORotate(new Vector3(90, 0, 0), 0.5f).SetEase(Ease.InOutQuad);
}
else
{
cameraManager.sceneCamera.transform.DOMove(cameraManager.gameCamera.transform.position + new Vector3(0, 0, _baseHeight + _height), 0.5f).SetEase(Ease.InOutQuad);
}
}
public void endEdit()
{
isEditing = false;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5003e915880eb8e48967ab8231f6f228
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,200 @@
using System.Collections;
using UnityEngine;
using Ichni.RhythmGame;
using UnityEngine.InputSystem;
using DG.Tweening;
using UniRx;
using UniRx.Triggers;
namespace Ichni.Editor
{
public class QuickMover : MonoBehaviour
{
public static QuickMover instance;
[Header("Settings")]
public float moveSensitivity = 0.5f; // 移动灵敏度
[Header("References")]
public GameObject prefab;
public BoxCollider colliderX;
public BoxCollider colliderY;
public BoxCollider colliderZ;
// 运行时状态
private IHaveTransformSubmodule targetElement;
private Camera editorCamera => EditorManager.instance.cameraManager.currentCamera;
private bool isMoving;
private int activeMoveCode = -1;
private Vector3 initialTargetPosition;
private Vector3 initialMouseScreenPos;
public GameElement targetGameElement =>
targetElement?.transformSubmodule?.attachedGameElement;
public void Initialize(IHaveTransformSubmodule targetElement)
{
this.targetElement = targetElement;
if (instance != null)
{
Destroy(instance.gameObject);
}
instance = this;
if (targetGameElement != null)
transform.position = targetGameElement.transform.position;
transform.localScale = Vector3.zero;
transform.DOScale(Vector3.one, 0.2f).SetEase(Ease.OutBack);
targetGameElement.OnDestroyAsObservable()
.Subscribe(_ =>
{
if (gameObject != null) Destroy(gameObject);
instance = null;
}).AddTo(this);
}
void Update()
{
if (isMoving) return;
transform.eulerAngles = targetGameElement.parentElement?.transform.eulerAngles ?? Vector3.zero;
transform.position = targetGameElement.transform.position;
if (Mouse.current.leftButton.wasPressedThisFrame)
{
Vector2 mousePosition = Mouse.current.position.ReadValue();
Ray ray = editorCamera.ScreenPointToRay(mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, float.MaxValue, LayerMask.GetMask("Selectable")))
{
if (hit.collider == colliderX)
{
colliderX.gameObject.transform.DOScale(colliderX.gameObject.transform.localScale * 1.2f, 0.2f);
StartMovement(0);
}
else if (hit.collider == colliderY)
{
colliderY.gameObject.transform.DOScale(colliderY.gameObject.transform.localScale * 1.2f, 0.2f);
StartMovement(1);
}
else if (hit.collider == colliderZ)
{
colliderZ.gameObject.transform.DOScale(colliderZ.gameObject.transform.localScale * 1.2f, 0.2f);
StartMovement(2);
}
}
}
}
private Vector3 objPosOffset;
private void StartMovement(int moveCode)
{
Debug.Log($"StartMovement: {moveCode}");
if (targetGameElement == null) return;
activeMoveCode = moveCode;
initialTargetPosition = targetGameElement.transform.position;
initialMouseScreenPos = Mouse.current.position.ReadValue();
objPosOffset = targetElement.transformSubmodule.currentPosition - targetElement.transformSubmodule.originalPosition;
StartCoroutine(MovementRoutine());
}
private IEnumerator MovementRoutine()
{
isMoving = true;
while (Mouse.current.leftButton.isPressed)
{
UpdateTargetPosition();
targetElement.transformSubmodule.originalPosition = targetElement.transformSubmodule.currentPosition - objPosOffset;
yield return null;
}
// 当鼠标释放时,结束移动
EndMovement();
isMoving = false;
activeMoveCode = -1;
}
private void EndMovement()
{
targetGameElement.transform.position = transform.position;
targetElement.transformSubmodule.currentPosition = targetGameElement.transform.localPosition;
targetElement.transformSubmodule.originalPosition = targetElement.transformSubmodule.currentPosition - objPosOffset;
targetGameElement.Refresh();
if (activeMoveCode == 0)
{
colliderX.gameObject.transform.DOScale(colliderX.gameObject.transform.localScale / 1.2f, 0.2f);
// Do something specific for X axis
}
else if (activeMoveCode == 1)
{
colliderY.gameObject.transform.DOScale(colliderY.gameObject.transform.localScale / 1.2f, 0.2f);
// Do something specific for Y axis
}
else if (activeMoveCode == 2)
{
colliderZ.gameObject.transform.DOScale(colliderZ.gameObject.transform.localScale / 1.2f, 0.2f);
// Do something specific for Z axis
}
}
private void UpdateTargetPosition()
{
if (targetGameElement == null) return;
// 获取当前鼠标位置
Vector3 currentMousePos = Mouse.current.position.ReadValue();
Vector3 mouseDelta = currentMousePos - initialMouseScreenPos;
// 计算移动轴的世界方向
Vector3 worldAxis = GetWorldAxis(activeMoveCode);
// 计算屏幕移动向量对应的世界移动量
float worldDelta = CalculateWorldDelta(worldAxis, mouseDelta);
// 应用新位置
Vector3 newPosition = initialTargetPosition + worldAxis * worldDelta;
targetGameElement.transform.position = newPosition;
targetGameElement.transform.localPosition = new Vector3(
Mathf.Round(targetGameElement.transform.localPosition.x * 100f) / 100f,
Mathf.Round(targetGameElement.transform.localPosition.y * 100f) / 100f,
Mathf.Round(targetGameElement.transform.localPosition.z * 100f) / 100f
);
transform.position = newPosition;
}
private Vector3 GetWorldAxis(int moveCode)
{
return moveCode switch
{
0 => transform.right, // X轴
1 => transform.up, // Y轴
2 => transform.forward, // Z轴
_ => Vector3.zero
};
}
private float CalculateWorldDelta(Vector3 worldAxis, Vector3 mouseDelta)
{
// 将世界轴转换为屏幕方向
Vector3 screenRefPoint = editorCamera.WorldToScreenPoint(initialTargetPosition);
Vector3 screenAxisEnd = editorCamera.WorldToScreenPoint(initialTargetPosition + worldAxis);
// 计算屏幕方向向量
Vector3 screenAxis = (screenAxisEnd - screenRefPoint).normalized;
// 防止摄像机与轴平行时出现零向量
if (screenAxis.sqrMagnitude < 0.01f) return 0;
// 计算鼠标移动在轴方向上的投影
Vector2 screenAxis2D = new Vector2(screenAxis.x, screenAxis.y);
Vector2 mouseDelta2D = new Vector2(mouseDelta.x, mouseDelta.y);
float projection = Vector2.Dot(mouseDelta2D, screenAxis2D.normalized);
// 应用灵敏度并返回
return projection * moveSensitivity * 0.01f;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cde63278924ce8944b8accfbf000a7e5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,80 @@
using System.Collections;
using System.Collections.Generic;
using Ichni;
using Ichni.RhythmGame;
using Michsky.MUIP;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public class NotefabContoler : MonoBehaviour
{
public SampleWindow sampleWindow;
public NoteBase noteBase;
public RawImage ifHold;
public void Initialize(NoteBase note, float timePerBeat, int beatDeviver, float posX)
{
noteBase = note;
Image color = GetComponent<Image>();
transform.localPosition = new Vector3(posX, note.exactJudgeTime / timePerBeat * beatDeviver, 0);
switch (note)
{
case Hold hold:
color.color = new Color(0, 1, 0, 1);
if (ifHold != null)
{
ifHold.transform.localPosition = new Vector3(0, (hold.holdEndTime - hold.exactJudgeTime) / timePerBeat * beatDeviver / 2, 0);
var rect = ifHold.GetComponent<RectTransform>();
rect.sizeDelta = new Vector2(rect.sizeDelta.x, (hold.holdEndTime - hold.exactJudgeTime) / timePerBeat * beatDeviver);
ifHold.color = new Color(0, 1, 0, 1);
}
break;
case Tap:
color.color = new Color(0, 1, 1, 1);
break;
case Stay:
color.color = new Color(1, 1, 0, 1);
break;
case Flick:
color.color = new Color(1, 0.2f, 0, 1);
break;
}
}
public void Update()
{
if (RectTransformUtility.RectangleContainsScreenPoint(this.GetComponent<RectTransform>(), Mouse.current.position.ReadValue()))
{
if (Mouse.current.leftButton.wasPressedThisFrame)
{
if (sampleWindow.isExpand) StartCoroutine(Moving());
if (EditorManager.instance.uiManager.inspector.connectedGameElement != noteBase) EditorManager.instance.uiManager.hierarchy.FindTab(noteBase);
}
}
}
public IEnumerator Moving()
{
sampleWindow.GetComponent<WindowDragger>().Lock = true;
float startX = transform.localPosition.x;
while (Mouse.current.leftButton.isPressed)
{
Vector2 localMousePosition = GetComponent<RectTransform>().InverseTransformPoint(Mouse.current.position.ReadValue());
// if (Mathf.Abs(localMousePosition.x - startX) > GetComponent<RectTransform>().sizeDelta.x / 2)
{
transform.localPosition += new Vector3(Mouse.current.delta.ReadValue().x, 0, 0);
}
yield return null;
}
noteBase.noteVisual.transformSubmodule.originalPosition = new Vector3(
transform.localPosition.x / sampleWindow.XWidth,
noteBase.noteVisual.transformSubmodule.originalPosition.y,
noteBase.noteVisual.transformSubmodule.originalPosition.z
);
noteBase.noteVisual.transformSubmodule.Refresh();
sampleWindow.GetComponent<WindowDragger>().Lock = false;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ae19b314bbfece546a1a8d74d38387ac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,277 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Dreamteck.Splines;
using Ichni;
using Ichni.Editor;
using Ichni.RhythmGame;
using TMPro;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public class SampleWindow : MovableWindow//该window高度为300横的要XWidth0和500之间切换
{
public static List<SampleWindow> instances = new List<SampleWindow>();
public TMP_InputField DeviverInputField;
public TMP_InputField verticalInputField;
public TMP_InputField horizontalInputField;
public RectTransform LineMovepoint;
public RectTransform NoteMovepoint;
public List<NoteBase> noteBases;
public bool isFocus = false;
public bool isExpand = false;
public int beatDeviver = 100;
public int XWidth = 10;
public int Xdevide = 1;
public float realDevider;
public GameObject beatLinePrefabv;
public GameObject beatLinePrefabh;
public GameObject NotePrefab;
float songTime => EditorManager.instance.songInformation.songTime;
float songBeat => EditorManager.instance.songInformation.songBeat;
float beatmapStartTime => -EditorManager.instance.songInformation.delay;
float timePerBeat => 60f / EditorManager.instance.songInformation.bpm;
public GameElement gameElement;
public SplinePositioner trackPositioner = null;
public GameObject trackHead;
public void Initialize(GameElement qgameElement, string title)
{
closeButton.onClick.AddListener(() =>
{
instances.Remove(this);
});
if (instances.Where(i => i.gameElement == qgameElement).Count() != 0)
{
Destroy(this.gameObject);
foreach (SampleWindow i in instances)
{
i.StartCoroutine(WindowAnim.Shake(instances.Where(i => i.gameElement == qgameElement).First().windowRect.gameObject));
}
return;
}
this.gameElement = qgameElement;
if (qgameElement is Track track)
{
trackPositioner = this.gameObject.AddComponent<SplinePositioner>();
trackPositioner.spline = track.trackPathSubmodule.path;
trackPositioner.enabled = isFocus;
trackPositioner.targetObject = trackHead;
}
InitializeWindow(title, null, new Vector3(1.2f, 1.2f, 1.2f));
//
SpawnBeatline();
OnceSpawnNote();
instances.Add(this);
}
public void SpawnBeatline()//添加优化措施
{
for (int i = LineMovepoint.childCount - 1; i >= 0; i--)
{
Destroy(LineMovepoint.GetChild(i).gameObject);
}
for (int i = 0; i < (int)EditorManager.instance.songInformation.song.length / timePerBeat; i++)
{
for (int j = 1; j < Xdevide; j++)
{
GameObject v = Instantiate(beatLinePrefabh, LineMovepoint);
v.transform.localPosition = new Vector3(0, i * beatDeviver + (beatDeviver / Xdevide * j), 0);
RawImage g = v.GetComponent<RawImage>();
g.color = new Color(0, g.color.g, g.color.b, 0.2f);
if (v.transform.localPosition.y > 1200)
{
Destroy(v);
break;
}
}
GameObject u = Instantiate(beatLinePrefabh, LineMovepoint);
u.transform.localPosition = new Vector3(0, i * beatDeviver, 0);
if (u.transform.localPosition.y > 600)
{
Destroy(u);
break;
}
}
}
public void OnceSpawnNote()
{
if (gameElement is Track track) noteBases = track.GetAllNotes();
else if (gameElement is ElementFolder elementFolder) noteBases = elementFolder.GetAllNotes();
for (int i = NoteMovepoint.childCount - 1; i >= 0; i--)
{
Destroy(NoteMovepoint.GetChild(i).gameObject);
}
foreach (var i in noteBases)
{
SpawnNote(i, i.noteVisual.transformSubmodule.originalPosition.x * XWidth);
}
}
private void SpawnNote(NoteBase i, float posx = 0)
{
GameObject u = Instantiate(NotePrefab, NoteMovepoint);
u.GetComponent<NotefabContoler>().Initialize(i, timePerBeat, beatDeviver, posx);
u.GetComponent<NotefabContoler>().sampleWindow = this;
}
public GameObject selectedGameObject;
void Update()
{
selectedGameObject = EventSystem.current.currentSelectedGameObject;
LineMovepoint.localPosition = new(0, -beatDeviver * (songBeat - (int)songBeat), 0);
NoteMovepoint.localPosition = new(0, -beatDeviver * songBeat, 0);
if (RectTransformUtility.RectangleContainsScreenPoint(windowRect, Mouse.current.position.ReadValue()) &&
selectedGameObject != DeviverInputField.gameObject &&
selectedGameObject != verticalInputField.gameObject &&
selectedGameObject != horizontalInputField.gameObject)
{
DetectNote();
}
}
void LateUpdate()
{
if (isFocus && gameElement is Track track)
{
if (track.trackTimeSubmodule is TrackTimeSubmoduleMovable trackTimeSubmoduleMovable)
{
trackPositioner.SetPercent(track.trackTimeSubmodule.headPercent);
windowRect.GetComponent<CanvasGroup>().alpha = (songTime >= trackTimeSubmoduleMovable.trackStartTime && songTime <= trackTimeSubmoduleMovable.trackEndTime) ? 1f : 0.2f;
}
else if (track.trackTimeSubmodule is TrackTimeSubmoduleStatic)
{
trackPositioner.SetPercent(0f);
}
TransformChanged();
windowRect.GetComponent<CanvasGroup>().alpha = track.timeDurationSubmodule.CheckTimeInDuration(songTime) ? 1f : 0.2f;
}
}
void TransformChanged()
{
RectTransform canvasRect = EditorManager.instance.inspectorCanvas.GetComponent<RectTransform>();
Vector2 ScreenPosition = EditorManager.instance.cameraManager.currentCamera.WorldToScreenPoint(trackHead.transform.position);
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, ScreenPosition, null, out Vector2 uiPosition))
{
windowRect.anchoredPosition = new Vector2(uiPosition.x, uiPosition.y + 150f);
}
}
public void ChangeFocus()
{
isFocus = !isFocus;
if (trackPositioner != null) trackPositioner.enabled = isFocus;
}
public void ChangeXdevide(string devide)
{
Xdevide = int.Parse(devide);
SpawnBeatline();
}
public void ChangeBeatdevide(string devide)
{
beatDeviver = int.Parse(devide);
SpawnBeatline();
OnceSpawnNote();
}
public void ChangeExpand()
{
if (isExpand)
{
isExpand = false;
windowRect.sizeDelta = new Vector2(100, windowRect.sizeDelta.y);
}
else
{
isExpand = true;
windowRect.sizeDelta = new Vector2(500, windowRect.sizeDelta.y);
}
}
public void ChangeXWidth(string change)
{
XWidth = int.Parse(change);
OnceSpawnNote();
}
public void DetectNote()
{
if (Keyboard.current.digit1Key.wasPressedThisFrame)
AddNote(0);
else if (Keyboard.current.digit2Key.wasPressedThisFrame)
AddNote(1);
else if (Keyboard.current.digit3Key.wasPressedThisFrame)
AddNote(2);
else if (Keyboard.current.digit4Key.wasPressedThisFrame)
AddNote(3);
}
public void AddNote(int NoteCode)
{
if (!EditorManager.instance.useNotePrefab)
{
LogWindow.Log("Please enable \"Note Prefab\" in EditorManager", Color.red);
return;
}
// 获取鼠标在 NoteMovepoint 中的相对位置
Vector2 localMousePosition = NoteMovepoint.InverseTransformPoint(Mouse.current.position.ReadValue());
Debug.Log(localMousePosition);
float mouseBeat = localMousePosition.y / beatDeviver;
float far = 0f;
while (far < mouseBeat)
{
far += 1f / Xdevide;
}
far -= 1f / Xdevide;
float time = Mathf.Round(far * timePerBeat * 1000f) / 1000f;
switch (NoteCode)
{
case 0:
Tap a = Tap.GenerateElement("New Tap", Guid.NewGuid(), new List<string>(), true, gameElement, time);
noteBases.Add(a);
a.noteVisual.transformSubmodule.originalPosition = new Vector3(isExpand ? (localMousePosition.x / XWidth) : 0f, 0f, 0f);
a.noteVisual.transformSubmodule.Refresh();
SpawnNote(a, isExpand ? localMousePosition.x : 0f);
break;
case 3:
Hold b = Hold.GenerateElement("New Hold", Guid.NewGuid(), new List<string>(), true, gameElement, time, time + 0.5f);
noteBases.Add(b);
b.noteVisual.transformSubmodule.originalPosition = new Vector3(isExpand ? (localMousePosition.x / XWidth) : 0f, 0f, 0f);
b.noteVisual.transformSubmodule.Refresh();
SpawnNote(b, isExpand ? localMousePosition.x : 0f);
break;
case 1:
Stay c = Stay.GenerateElement("New Stay", Guid.NewGuid(), new List<string>(), true, gameElement, time);
noteBases.Add(c);
c.noteVisual.transformSubmodule.originalPosition = new Vector3(isExpand ? (localMousePosition.x / XWidth) : 0f, 0f, 0f);
c.noteVisual.transformSubmodule.Refresh();
SpawnNote(c, isExpand ? localMousePosition.x : 0f);
break;
case 2:
Flick d = Flick.GenerateElement("New Flick", Guid.NewGuid(), new List<string>(), true, gameElement, time, new List<Vector2>());
noteBases.Add(d);
d.noteVisual.transformSubmodule.originalPosition = new Vector3(isExpand ? (localMousePosition.x / XWidth) : 0f, 0f, 0f);
d.noteVisual.transformSubmodule.Refresh();
SpawnNote(d, isExpand ? localMousePosition.x : 0f);
break;
default:
break;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a48a638548bdb6645bfa74867dc72087
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: