Netcode Bootstrap
This commit is contained in:
+907
@@ -0,0 +1,907 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Rukhanka.Toolbox;
|
||||
using Rukhanka.WaybackMachine;
|
||||
using Unity.Assertions;
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
namespace Rukhanka.Editor
|
||||
{
|
||||
public partial class WaybackMachineWindow: EditorWindow
|
||||
{
|
||||
[SerializeField]
|
||||
private VisualTreeAsset windowAsset = default;
|
||||
[SerializeField]
|
||||
private VisualTreeAsset leftPaneAsset = default;
|
||||
[SerializeField]
|
||||
private VisualTreeAsset rightPaneAsset = default;
|
||||
[SerializeField]
|
||||
private VisualTreeAsset stateInfoAsset = default;
|
||||
[SerializeField]
|
||||
private VisualTreeAsset eventInfoAsset = default;
|
||||
[SerializeField]
|
||||
private VisualTreeAsset animInfoAsset = default;
|
||||
|
||||
TwoPaneSplitView splitView;
|
||||
MinMaxSlider minMaxSlider;
|
||||
TimelineHeader timelineHeader;
|
||||
TimelineContent timelineContent;
|
||||
NativeReference<TimelinePortalData> timelinePortal;
|
||||
VisualElement recordBtn;
|
||||
ToolbarBreadcrumbs entityPath;
|
||||
ToolbarButton entityPathWorld, entityPathEntity;
|
||||
ToolbarButton globalSettingsButton;
|
||||
ToolbarButton previewBtn;
|
||||
Label memoryStat;
|
||||
ToolbarButton saveBtn;
|
||||
|
||||
VisualElement leftPane, rightPane;
|
||||
|
||||
Label statesHeader;
|
||||
VisualElement statesBarBodyVE;
|
||||
Button stateSettingsButton;
|
||||
|
||||
Label animationsHeader;
|
||||
VisualElement animBarBodyVE;
|
||||
Button animationSettingsButton;
|
||||
|
||||
Label eventsHeader;
|
||||
VisualElement eventsBarBodyVE;
|
||||
Button eventSettingsButton;
|
||||
|
||||
List<VisualElement> stateInfoWidgets = new ();
|
||||
List<VisualElement> eventInfoWidgets = new ();
|
||||
List<VisualElement> animInfoWidgets = new ();
|
||||
|
||||
int selectedWorldIndex = -1;
|
||||
Entity selectedEntity = Entity.Null;
|
||||
|
||||
Action<PlayModeStateChange> playModeChangeFn;
|
||||
NativeReference<WaybackMachineData> recordedData;
|
||||
EntityQuery recordSingletonEq, playbackSingletonEq, animationDatabaseSingletonEq;
|
||||
|
||||
int prevKnobFrame = 1000000;
|
||||
WaybackMachineSettings settings;
|
||||
|
||||
public const string iconPath = "Packages/com.rukhanka.animation/Rukhanka.Editor/Editor Default Resources/Icons/RukhankaWaybackMachine@16.png";
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
[MenuItem("Window/Rukhanka Animation/Animation Wayback Machine")]
|
||||
public static void ShowExample()
|
||||
{
|
||||
var wnd = GetWindow<WaybackMachineWindow>();
|
||||
var icon = AssetDatabase.LoadAssetAtPath(iconPath, typeof(Texture)) as Texture;
|
||||
wnd.titleContent = new GUIContent("Rukhanka Wayback Machine", icon);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void SetupIconImage(Button button, Background img)
|
||||
{
|
||||
#if !UNITY_2023_2_OR_NEWER
|
||||
var icon = button.Q<Image>();
|
||||
if(icon == null)
|
||||
button.Add(icon = new Image() {name = "LegacyIcon" });
|
||||
icon.image = img.texture;
|
||||
#else
|
||||
button.iconImage = img;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public unsafe void CreateGUI()
|
||||
{
|
||||
playModeChangeFn = (playModeStateChange) =>
|
||||
{
|
||||
PlayModeStateChanged(this);
|
||||
};
|
||||
EditorApplication.playModeStateChanged += playModeChangeFn;
|
||||
|
||||
settings = WaybackMachineSettings.MakeDefault();
|
||||
|
||||
recordedData = new (Allocator.Persistent);
|
||||
recordedData.GetUnsafePtr()->Construct();
|
||||
|
||||
timelinePortal = new (Allocator.Persistent);
|
||||
timelinePortal.GetUnsafePtr()->Construct();
|
||||
|
||||
// Each editor window contains a root VisualElement object
|
||||
var root = rootVisualElement;
|
||||
var doc = windowAsset.Instantiate();
|
||||
root.Add(doc);
|
||||
|
||||
splitView = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);
|
||||
splitView.RegisterCallback<GeometryChangedEvent>(OnGeometryChange);
|
||||
root.Add(splitView);
|
||||
|
||||
leftPane = leftPaneAsset.Instantiate();
|
||||
rightPane = rightPaneAsset.Instantiate();
|
||||
splitView.Add(leftPane);
|
||||
splitView.Add(rightPane);
|
||||
|
||||
var tlh = rightPane.Q<VisualElement>("timeline");
|
||||
timelineHeader = new TimelineHeader();
|
||||
timelineHeader.timelinePortal = timelinePortal;
|
||||
tlh.Add(timelineHeader);
|
||||
|
||||
var gearIconBackground = new Background() { texture = (Texture2D)EditorGUIUtility.IconContent("_Popup").image };
|
||||
|
||||
animBarBodyVE = leftPane.Q<VisualElement>("animbody");
|
||||
animationsHeader = leftPane.Q<Label>("animheader");
|
||||
animationSettingsButton = animationsHeader.Q<Button>("settings");
|
||||
SetupIconImage(animationSettingsButton, gearIconBackground);
|
||||
animationSettingsButton.clickable.clicked += SelectAnimationOptions;
|
||||
animationsHeader.RegisterCallback<MouseDownEvent>(_ => ToggleVisibility(ref settings.animationsVisible, animationsHeader, animBarBodyVE));
|
||||
|
||||
statesBarBodyVE = leftPane.Q<VisualElement>("smbody");
|
||||
statesHeader = leftPane.Q<Label>("smheader");
|
||||
stateSettingsButton = statesHeader.Q<Button>("settings");
|
||||
SetupIconImage(stateSettingsButton, gearIconBackground);
|
||||
statesHeader.RegisterCallback<MouseDownEvent>(_ => ToggleVisibility(ref settings.statesVisible, statesHeader, statesBarBodyVE));
|
||||
|
||||
eventsBarBodyVE = leftPane.Q<VisualElement>("eventsbody");
|
||||
eventsHeader = leftPane.Q<Label>("eventsheader");
|
||||
eventSettingsButton = eventsHeader.Q<Button>("settings");
|
||||
SetupIconImage(eventSettingsButton, gearIconBackground);
|
||||
eventSettingsButton.clickable.clicked += SelectEventOptions;
|
||||
eventsHeader.RegisterCallback<MouseDownEvent>(_ => ToggleVisibility(ref settings.eventsVisible, eventsHeader, eventsBarBodyVE));
|
||||
|
||||
var tlc = rightPane.Q<VisualElement>("content");
|
||||
timelineContent = new TimelineContent();
|
||||
timelineContent.timelinePortal = timelinePortal;
|
||||
timelineContent.recordedData = recordedData;
|
||||
timelineContent.textMeasurer = rightPane.Q<Label>("text-measurer");
|
||||
timelineContent.RegisterCallback<WheelEvent>(MouseScrollOnTimeline);
|
||||
tlc.Add(timelineContent);
|
||||
|
||||
minMaxSlider = rightPane.Q<MinMaxSlider>("slider");
|
||||
minMaxSlider.minValue = minMaxSlider.lowLimit;
|
||||
minMaxSlider.maxValue = minMaxSlider.highLimit;
|
||||
|
||||
recordBtn = doc.Q<ToolbarButton>("record-btn");
|
||||
recordBtn.generateVisualContent += DrawRecordBtn;
|
||||
recordBtn.RegisterCallback<ClickEvent>(ToggleRecording);
|
||||
|
||||
entityPath = doc.Q<ToolbarBreadcrumbs>("entity-path");
|
||||
entityPath.PushItem(DEFAULT_WORLD_TEXT);
|
||||
entityPathWorld = entityPath[0] as ToolbarButton;
|
||||
entityPathWorld.RegisterCallback<MouseDownEvent>(SelectWorldMenu, TrickleDown.TrickleDown);
|
||||
|
||||
entityPath.PushItem(DEFAULT_ENTITY_TEXT);
|
||||
entityPathEntity = entityPath[1] as ToolbarButton;
|
||||
entityPathEntity.RegisterCallback<MouseDownEvent>(SelectEntityMenu, TrickleDown.TrickleDown);
|
||||
|
||||
globalSettingsButton = doc.Q<ToolbarButton>("settings-btn");
|
||||
SetupIconImage(globalSettingsButton, gearIconBackground);
|
||||
globalSettingsButton.RegisterCallback<MouseDownEvent>(SelectGlobalOptions, TrickleDown.TrickleDown);
|
||||
|
||||
previewBtn = doc.Q<ToolbarButton>("preview-btn");
|
||||
previewBtn.RegisterCallback<ClickEvent>(TogglePreview, TrickleDown.TrickleDown);
|
||||
|
||||
saveBtn = doc.Q<ToolbarButton>("save-btn");
|
||||
var saveIcon = new Background() { texture = (Texture2D)EditorGUIUtility.IconContent("SaveAs").image };
|
||||
SetupIconImage(saveBtn, saveIcon);
|
||||
saveBtn.clickable.clicked += SaveRecordedData;
|
||||
|
||||
var importBtn = doc.Q<ToolbarButton>("import-btn");
|
||||
var importIcon = new Background() { texture = (Texture2D)EditorGUIUtility.IconContent("Import").image };
|
||||
SetupIconImage(importBtn, importIcon);
|
||||
importBtn.clickable.clicked += LoadRecordedData;
|
||||
|
||||
memoryStat = doc.Q<Label>("memorystat");
|
||||
#if !RUKHANKA_DEBUG_INFO
|
||||
var noDebugInfoWarningLabel = doc.Q<Label>("nodebuginfowarning");
|
||||
noDebugInfoWarningLabel.text = "Enable 'Debug and Validation Mode' for object names";
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void PlayModeStateChanged(WaybackMachineWindow w)
|
||||
{
|
||||
if (w != null)
|
||||
w.ResetEntityPath();
|
||||
|
||||
w.DisposeEntityQueries();
|
||||
w.StopRecord();
|
||||
w.StopPreview();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
unsafe void DisposeEntityQueries()
|
||||
{
|
||||
if (animationDatabaseSingletonEq.__impl != null)
|
||||
animationDatabaseSingletonEq.Dispose();
|
||||
if (playbackSingletonEq.__impl != null)
|
||||
playbackSingletonEq.Dispose();
|
||||
if (recordSingletonEq.__impl != null)
|
||||
recordSingletonEq.Dispose();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void ResetEntityPath()
|
||||
{
|
||||
selectedWorldIndex = -1;
|
||||
selectedEntity = Entity.Null;
|
||||
entityPathEntity.text = DEFAULT_ENTITY_TEXT;
|
||||
entityPathWorld.text = DEFAULT_WORLD_TEXT;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
World GetSelectedWorld()
|
||||
{
|
||||
if (selectedWorldIndex < 0 || selectedWorldIndex >= World.All.Count)
|
||||
return null;
|
||||
|
||||
return World.All[selectedWorldIndex];
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
string GetEntityName(EntityManager em, Entity e)
|
||||
{
|
||||
var eName = $"{em.GetName(e)} {e}";
|
||||
return eName;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void SelectEntityMenu(MouseDownEvent evt)
|
||||
{
|
||||
var world = GetSelectedWorld();
|
||||
if (world == null)
|
||||
return;
|
||||
|
||||
var eq = new EntityQueryBuilder(Allocator.Temp)
|
||||
.WithAll<RigDefinitionComponent>()
|
||||
.Build(world.EntityManager);
|
||||
|
||||
if (eq.IsEmpty)
|
||||
return;
|
||||
|
||||
var entities = eq.ToEntityArray(Allocator.Temp);
|
||||
var m = new GenericDropdownMenu();
|
||||
|
||||
for (var i = 0; i < entities.Length; ++i)
|
||||
{
|
||||
var e = entities[i];
|
||||
var eName = GetEntityName(world.EntityManager, e);
|
||||
m.AddItem(eName, false, _ => SelectEntity(e), null);
|
||||
}
|
||||
DropDown(m, entityPathEntity);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void SelectAnimationOptions()
|
||||
{
|
||||
var m = new GenericDropdownMenu();
|
||||
|
||||
m.AddItem("Time Graphs", settings.animationTimeGraphs, _ => settings.animationTimeGraphs = !settings.animationTimeGraphs, null);
|
||||
m.AddItem("Weight Graphs", settings.animationWeightGraphs, _ => settings.animationWeightGraphs = !settings.animationWeightGraphs, null);
|
||||
DropDown(m, animationSettingsButton);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void SelectEventOptions()
|
||||
{
|
||||
var m = new GenericDropdownMenu();
|
||||
|
||||
m.AddItem("Event Labels", settings.eventLabels, _ => settings.eventLabels = !settings.eventLabels, null);
|
||||
DropDown(m, eventSettingsButton);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void SelectGlobalOptions(MouseDownEvent evt)
|
||||
{
|
||||
var m = new GenericDropdownMenu();
|
||||
|
||||
m.AddItem("Seconds", settings.rulerMode == WaybackMachineSettings.RulerMode.Seconds, _ => settings.rulerMode = WaybackMachineSettings.RulerMode.Seconds, null);
|
||||
m.AddItem("Frames", settings.rulerMode == WaybackMachineSettings.RulerMode.Frames, _ => settings.rulerMode = WaybackMachineSettings.RulerMode.Frames, null);
|
||||
m.AddSeparator("Sample Rate");
|
||||
m.AddItem("120 FPS", recordedData.Value.fpsMode == WaybackMachineData.FPSMode.FPS120, _ => ChangeFrameDuration(WaybackMachineData.FPSMode.FPS120), null);
|
||||
m.AddItem("60 FPS", recordedData.Value.fpsMode == WaybackMachineData.FPSMode.FPS60, _ => ChangeFrameDuration(WaybackMachineData.FPSMode.FPS60), null);
|
||||
m.AddItem("30 FPS", recordedData.Value.fpsMode == WaybackMachineData.FPSMode.FPS30, _ => ChangeFrameDuration(WaybackMachineData.FPSMode.FPS30), null);
|
||||
DropDown(m, globalSettingsButton);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void SelectWorldMenu(MouseDownEvent evt)
|
||||
{
|
||||
var m = new GenericDropdownMenu();
|
||||
for (var i = 0; i < World.All.Count; ++i)
|
||||
{
|
||||
var world = World.All[i];
|
||||
var worldIndex = i;
|
||||
m.AddItem(world.Name, false, _ => SelectWorld(worldIndex), null);
|
||||
}
|
||||
DropDown(m, entityPathWorld);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void SelectEntity(Entity e)
|
||||
{
|
||||
var world = GetSelectedWorld();
|
||||
if (world == null)
|
||||
return;
|
||||
|
||||
var eName = GetEntityName(world.EntityManager, e);
|
||||
entityPathEntity.text = eName;
|
||||
selectedEntity = e;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void SelectWorld(int worldIndex)
|
||||
{
|
||||
var prevWorldIndex = selectedWorldIndex;
|
||||
|
||||
var world = World.All[worldIndex];
|
||||
selectedWorldIndex = worldIndex;
|
||||
entityPathWorld.text = world.Name;
|
||||
|
||||
// Create queries
|
||||
recordSingletonEq = new EntityQueryBuilder(Allocator.Temp)
|
||||
.WithAll<RecordComponent>()
|
||||
.Build(world.EntityManager);
|
||||
|
||||
playbackSingletonEq = new EntityQueryBuilder(Allocator.Temp)
|
||||
.WithAllRW<PlaybackComponent>()
|
||||
.Build(world.EntityManager);
|
||||
|
||||
animationDatabaseSingletonEq = new EntityQueryBuilder(Allocator.Temp)
|
||||
.WithAll<BlobDatabaseSingleton>()
|
||||
.Build(world.EntityManager);
|
||||
|
||||
if (prevWorldIndex != worldIndex)
|
||||
{
|
||||
// Reset entity
|
||||
selectedEntity = Entity.Null;
|
||||
entityPathEntity.text = DEFAULT_ENTITY_TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void OnGeometryChange(GeometryChangedEvent evt)
|
||||
{
|
||||
Update();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void MouseScrollOnTimeline(WheelEvent evt)
|
||||
{
|
||||
// Make scroll amount constant in screen space
|
||||
var minT = timelinePortal.Value.GetFrameForPosX(0);
|
||||
var maxT = timelinePortal.Value.GetFrameForPosX(evt.delta.y * TIMELINE_MOUSE_SCROLL_SPEED);
|
||||
var dT = maxT - minT;
|
||||
|
||||
var f = (evt.mousePosition.x - timelineContent.worldBound.xMin) / timelineContent.worldBound.width;
|
||||
minMaxSlider.minValue -= dT * f;
|
||||
minMaxSlider.maxValue += dT * (1 - f);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
unsafe void ChangeFrameDuration(WaybackMachineData.FPSMode newMode)
|
||||
{
|
||||
if (recordedData.Value.fpsMode == newMode)
|
||||
return;
|
||||
|
||||
// FPS cannot be changed for existing recordings, so warn user about recorded data reset
|
||||
if (recordedData.Value.lastRecordedFrame > 0 && !EditorUtility.DisplayDialog("Wayback Machine", "Sample rate cannot be changed for existing recording. Confirm to clear recorded data.", "OK", "Cancel"))
|
||||
return;
|
||||
|
||||
recordedData.GetUnsafePtr()->Clear();
|
||||
recordedData.GetUnsafePtr()->fpsMode = newMode;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
unsafe void Update()
|
||||
{
|
||||
if (recordedData.IsCreated && recordedData.Value.lastRecordedFrame > 0)
|
||||
{
|
||||
var numFrames = recordedData.Value.lastRecordedFrame;
|
||||
if (numFrames > minMaxSlider.highLimit)
|
||||
{
|
||||
var dh = numFrames - minMaxSlider.highLimit;
|
||||
minMaxSlider.highLimit = numFrames;
|
||||
var v = minMaxSlider.value;
|
||||
v.x += dh;
|
||||
minMaxSlider.value = v;
|
||||
}
|
||||
if (IsRecording() && numFrames > minMaxSlider.value.y)
|
||||
{
|
||||
var v = minMaxSlider.value;
|
||||
v.y = numFrames;
|
||||
minMaxSlider.value = v;
|
||||
}
|
||||
}
|
||||
|
||||
if (minMaxSlider.value.y - minMaxSlider.value.x < 4)
|
||||
{
|
||||
var v = minMaxSlider.value;
|
||||
v.y = v.x + 4;
|
||||
minMaxSlider.value = v;
|
||||
}
|
||||
timelinePortal.GetUnsafePtr()->frameSizeInSec = recordedData.Value.GetFrameDuration();
|
||||
timelinePortal.GetUnsafePtr()->visibleRange = minMaxSlider.value;
|
||||
timelinePortal.GetUnsafePtr()->frameRange = new Vector2(minMaxSlider.lowLimit, minMaxSlider.highLimit);
|
||||
timelinePortal.GetUnsafePtr()->contentWidth = timelineHeader.contentRect.width;
|
||||
timelinePortal.GetUnsafePtr()->ComputeTicks();
|
||||
|
||||
timelineContent.animHeaderHeight = animationsHeader.localBound.height;
|
||||
timelineContent.eventsHeaderHeight = eventsHeader.localBound.height;
|
||||
timelineContent.statesHeaderHeight = statesHeader.localBound.height;
|
||||
timelineContent.eventBarsCount = 0;
|
||||
timelineContent.animBarsCount = 0;
|
||||
timelineContent.stateBarsCount = 0;
|
||||
|
||||
timelineHeader.settings = settings;
|
||||
timelineContent.settings = settings;
|
||||
timelineContent.ComputeShapes();
|
||||
|
||||
UpdateInfoWidgetCounts();
|
||||
|
||||
timelineHeader.MarkDirtyRepaint();
|
||||
timelineContent.MarkDirtyRepaint();
|
||||
|
||||
UpdatePreview();
|
||||
UpdateKnobTimeInfoPanes();
|
||||
UpdateMemoryStat();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void DrawRecordBtn(MeshGenerationContext ctx)
|
||||
{
|
||||
var p = ctx.painter2D;
|
||||
var r = math.min(recordBtn.contentRect.width, recordBtn.contentRect.height) / 2 * 0.8f;
|
||||
var c = recordBtn.contentRect.center;
|
||||
|
||||
p.strokeColor = Color.white;
|
||||
p.BeginPath();
|
||||
p.Arc(c, r, Angle.Degrees(0), Angle.Degrees(360.0f));
|
||||
p.Stroke();
|
||||
|
||||
if (IsRecording())
|
||||
{
|
||||
p.fillColor = Color.red;
|
||||
p.BeginPath();
|
||||
p.Arc(c, r * 0.6f, Angle.Degrees(0), Angle.Degrees(360.0f));
|
||||
p.Fill();
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void ToggleVisibility(ref int visStatus, Label header, VisualElement body)
|
||||
{
|
||||
visStatus = visStatus ^ 1;
|
||||
var s = header.text;
|
||||
var arrowChar = visStatus != 0 ? '▼' : '◀';
|
||||
s = $"{s.Substring(0, s.Length - 1)}{arrowChar}";
|
||||
header.text = s;
|
||||
body.style.display = new StyleEnum<DisplayStyle>(visStatus != 0 ? DisplayStyle.Flex : DisplayStyle.None);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void ToggleRecording(ClickEvent evt)
|
||||
{
|
||||
if (IsRecording())
|
||||
StopRecord();
|
||||
else
|
||||
StartRecord();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void TogglePreview(ClickEvent evt)
|
||||
{
|
||||
if (IsPreview())
|
||||
StopPreview();
|
||||
else
|
||||
StartPreview();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void StartRecord()
|
||||
{
|
||||
var world = GetSelectedWorld();
|
||||
if (world == null || selectedEntity == Entity.Null)
|
||||
return;
|
||||
|
||||
timelineContent.isRecording = true;
|
||||
entityPath.SetEnabled(false);
|
||||
minMaxSlider.value = new Vector2(0, 1000);
|
||||
|
||||
var isRecording = IsRecording();
|
||||
Assert.IsFalse(isRecording);
|
||||
|
||||
// Adjust recording system rate manager
|
||||
var sg = world.GetExistingSystemManaged<WaybackMachineRecordSystemGroup>();
|
||||
if (sg != null)
|
||||
sg.RateManager.Timestep = recordedData.Value.GetFrameDuration();
|
||||
|
||||
recordedData.Value.Clear();
|
||||
var rc = new RecordComponent { wbData = recordedData };
|
||||
world.EntityManager.AddComponentData(selectedEntity, rc);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void StopRecord()
|
||||
{
|
||||
timelineContent.isRecording = false;
|
||||
entityPath.SetEnabled(true);
|
||||
|
||||
var isRecording = IsRecording();
|
||||
if (!isRecording)
|
||||
return;
|
||||
|
||||
var world = GetSelectedWorld();
|
||||
world.EntityManager.RemoveComponent<RecordComponent>(selectedEntity);
|
||||
|
||||
recordBtn.MarkDirtyRepaint();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void UpdateInfoWidgetCounts()
|
||||
{
|
||||
if (settings.IsEventsVisible())
|
||||
UpdateInfoWidgetCount(eventInfoWidgets, timelineContent.eventBarsCount, EVENT_ROW_HEIGHT, eventInfoAsset, eventsBarBodyVE);
|
||||
if (settings.IsStatesVisible())
|
||||
UpdateInfoWidgetCount(stateInfoWidgets, timelineContent.stateBarsCount, STATE_ROW_HEIGHT, stateInfoAsset, statesBarBodyVE);
|
||||
if (settings.IsAnimationsVisible())
|
||||
UpdateInfoWidgetCount(animInfoWidgets, timelineContent.animBarsCount, ANIMATION_ROW_HEIGHT, animInfoAsset, animBarBodyVE);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void UpdateInfoWidgetCount(List<VisualElement> elementsArr, int newCount, float widgetHeight, VisualTreeAsset template, VisualElement parent)
|
||||
{
|
||||
if (elementsArr.Count == newCount)
|
||||
return;
|
||||
|
||||
// Add if to few
|
||||
var l = elementsArr.Count;
|
||||
for (var i = l; i < newCount; ++i)
|
||||
{
|
||||
var siw = template.Instantiate();
|
||||
var bkg = siw.Q<VisualElement>("background");
|
||||
bkg.style.height = new StyleLength(widgetHeight);
|
||||
bkg.visible = false;
|
||||
parent.Add(bkg);
|
||||
elementsArr.Add(bkg);
|
||||
}
|
||||
|
||||
// Remove if too many
|
||||
for (var i = newCount; i < l; ++i)
|
||||
{
|
||||
var siw = elementsArr[i];
|
||||
parent.Remove(siw);
|
||||
}
|
||||
var countToRemove = l - newCount;
|
||||
if (countToRemove > 0)
|
||||
elementsArr.RemoveRange(newCount, countToRemove);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void UpdateKnobTimeInfoPanes()
|
||||
{
|
||||
var knobPos = timelinePortal.Value.GetKnobPosX();
|
||||
var knobFrame = timelinePortal.Value.knobFrame;
|
||||
if (prevKnobFrame == knobFrame)
|
||||
return;
|
||||
|
||||
foreach (var eiw in eventInfoWidgets) eiw.visible = false;
|
||||
foreach (var siw in stateInfoWidgets)
|
||||
{
|
||||
foreach (var c in siw.Children()) c.style.visibility = StyleKeyword.Null;
|
||||
siw.visible = false;
|
||||
}
|
||||
foreach (var aiw in animInfoWidgets) aiw.visible = false;
|
||||
|
||||
UpdateKnobTimeAnimationEventInfos(knobPos);
|
||||
UpdateKnobTimeAnimatorEventInfos(knobPos);
|
||||
UpdateKnobTimeStateInfos(knobPos);
|
||||
UpdateKnobTimeStateTransitionInfos(knobPos);
|
||||
UpdateKnobTimeAnimationInfos(knobPos, knobFrame);
|
||||
|
||||
prevKnobFrame = knobFrame;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void UpdateKnobTimeAnimationInfos(float knobPos, int knobFrame)
|
||||
{
|
||||
NativeList<int2> tmpIndicesList = new (0xff, Allocator.Temp);
|
||||
|
||||
timelineContent.GetAnimationShapeIndicesForPos(ref tmpIndicesList, knobPos);
|
||||
for (var i = 0; i < tmpIndicesList.Length; ++i)
|
||||
{
|
||||
var indexRow = tmpIndicesList[i];
|
||||
var h = recordedData.Value.animHistory[indexRow.x];
|
||||
var w = animInfoWidgets[indexRow.y];
|
||||
|
||||
w.Q<Label>("name").text = h.GetName();
|
||||
|
||||
var relativeKnobFrame = knobFrame - h.frameSpan.x;
|
||||
var weight = h.historyWeights[relativeKnobFrame].value;
|
||||
var weightColor = ColorTools.ToWebColor(ANIMATION_HISTORY_WEIGHT_LINE_COLOR);
|
||||
w.Q<Label>("weight").text = $"<color={weightColor}>Weight</color>: {weight:0.##}";
|
||||
|
||||
var time = h.historyAnimTime[relativeKnobFrame].value;
|
||||
var timeColor = ColorTools.ToWebColor(ANIMATION_HISTORY_TIME_LINE_COLOR);
|
||||
w.Q<Label>("time").text = $"<color={timeColor}>Time</color>: {time:0.##}";
|
||||
|
||||
w.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void UpdateKnobTimeStateInfos(float knobPos)
|
||||
{
|
||||
NativeList<int3> tmpIndicesList = new (0xff, Allocator.Temp);
|
||||
|
||||
timelineContent.GetStateShapeIndicesForPos(ref tmpIndicesList, knobPos);
|
||||
for (var i = 0; i < tmpIndicesList.Length; ++i)
|
||||
{
|
||||
var indexRowColor = tmpIndicesList[i];
|
||||
var h = recordedData.Value.controllerStateHistory[indexRowColor.x];
|
||||
var w = stateInfoWidgets[indexRowColor.y];
|
||||
var srcStateInfo = w.Q<VisualElement>("srcstateinfo");
|
||||
var dstStateInfo = w.Q<VisualElement>("dststateinfo");
|
||||
var transitionInfo = w.Q<VisualElement>("transitioninfo");
|
||||
|
||||
dstStateInfo.visible = false;
|
||||
transitionInfo.visible = false;
|
||||
|
||||
var color = ColorTools.FromInt(indexRowColor.z);
|
||||
srcStateInfo.Q<VisualElement>("color").style.backgroundColor = new StyleColor(color);
|
||||
srcStateInfo.Q<Label>("name").text = h.GetName();
|
||||
srcStateInfo.Q<Label>("id").text = h.stateId.ToString();
|
||||
|
||||
w.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void UpdateKnobTimeStateTransitionInfos(float knobPos)
|
||||
{
|
||||
NativeList<int4> tmpIndicesList = new (0xff, Allocator.Temp);
|
||||
|
||||
timelineContent.GetTransitionShapeIndicesForPos(ref tmpIndicesList, knobPos);
|
||||
for (var i = 0; i < tmpIndicesList.Length; ++i)
|
||||
{
|
||||
var indexRowColor = tmpIndicesList[i];
|
||||
var h = recordedData.Value.controllerTransitionHistory[indexRowColor.x];
|
||||
var w = stateInfoWidgets[indexRowColor.y];
|
||||
var srcStateInfo = w.Q<VisualElement>("srcstateinfo");
|
||||
var dstStateInfo = w.Q<VisualElement>("dststateinfo");
|
||||
var transitionInfo = w.Q<VisualElement>("transitioninfo");
|
||||
|
||||
var srcColor = ColorTools.FromInt(indexRowColor.z);
|
||||
var srcState = recordedData.Value.controllerStateHistory[h.srcStateDataIndex];
|
||||
srcStateInfo.visible = true;
|
||||
srcStateInfo.Q<VisualElement>("color").style.backgroundColor = new StyleColor(srcColor);
|
||||
srcStateInfo.Q<Label>("name").text = srcState.GetName();
|
||||
srcStateInfo.Q<Label>("id").text = h.srcStateId.ToString();
|
||||
|
||||
var dstColor = ColorTools.FromInt(indexRowColor.w);
|
||||
var dstState = recordedData.Value.controllerStateHistory[h.dstStateDataIndex];
|
||||
dstStateInfo.visible = true;
|
||||
dstStateInfo.Q<VisualElement>("color").style.backgroundColor = new StyleColor(dstColor);
|
||||
dstStateInfo.Q<Label>("name").text = dstState.GetName();
|
||||
dstStateInfo.Q<Label>("id").text = h.dstStateId.ToString();
|
||||
|
||||
transitionInfo.visible = true;
|
||||
transitionInfo.Q<Label>("name").text = h.GetName();
|
||||
transitionInfo.Q<Label>("id").text = h.transitionId.ToString();
|
||||
|
||||
w.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void UpdateKnobTimeAnimationEventInfos(float knobPos)
|
||||
{
|
||||
NativeList<int2> tmpIndicesList = new (0xff, Allocator.Temp);
|
||||
|
||||
timelineContent.GetAnimationEventShapeIndicesForPos(ref tmpIndicesList, knobPos);
|
||||
for (var i = 0; i < tmpIndicesList.Length; ++i)
|
||||
{
|
||||
var indexAndRow = tmpIndicesList[i];
|
||||
var h = recordedData.Value.animationEventHistory[indexAndRow.x];
|
||||
var w = eventInfoWidgets[indexAndRow.y];
|
||||
w.Q<Label>("name").text = h.GetName();
|
||||
w.Q<Label>("stringv").text = $"<color=green>S</color>: '{h.GetStringParam()}' <color=green>F</color>: {h.floatParam} <color=green>I</color>: {h.intParam}";
|
||||
w.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void UpdateKnobTimeAnimatorEventInfos(float knobPos)
|
||||
{
|
||||
NativeList<int2> tmpIndicesList = new (0xff, Allocator.Temp);
|
||||
|
||||
timelineContent.GetAnimatorEventShapeIndicesForPos(ref tmpIndicesList, knobPos);
|
||||
for (var i = 0; i < tmpIndicesList.Length; ++i)
|
||||
{
|
||||
var indexAndRow = tmpIndicesList[i];
|
||||
var h = recordedData.Value.animatorEventHistory[indexAndRow.x];
|
||||
var w = eventInfoWidgets[indexAndRow.y];
|
||||
var trinagleSymbol = "▶";
|
||||
var prefix = h.eventType == AnimatorControllerEventComponent.EventType.StateEnter ? trinagleSymbol : "";
|
||||
var suffix = h.eventType == AnimatorControllerEventComponent.EventType.StateExit ? trinagleSymbol : "";
|
||||
w.Q<Label>("name").text = $"{prefix}{h.name}{suffix}";
|
||||
w.Q<Label>("stringv").text = $"<color=green>LayerId</color>: {h.layerId} <color=green>StateID</color>: {h.stateId}";
|
||||
w.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void UpdateMemoryStat()
|
||||
{
|
||||
var mem = recordedData.Value.GetDataSize();
|
||||
memoryStat.text = $"Memory size {CommonTools.FormatMemory(mem)}";
|
||||
saveBtn.SetEnabled(mem > 0);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void UpdatePreview()
|
||||
{
|
||||
var world = GetSelectedWorld();
|
||||
if (world == null || selectedEntity == Entity.Null)
|
||||
return;
|
||||
|
||||
if (!IsPreview())
|
||||
return;
|
||||
|
||||
if (!animationDatabaseSingletonEq.TryGetSingleton<BlobDatabaseSingleton>(out var animDBSingleton))
|
||||
return;
|
||||
|
||||
var pc = playbackSingletonEq.GetSingletonRW<PlaybackComponent>();
|
||||
pc.ValueRW.playbackData.Clear();
|
||||
|
||||
var knobFrame = timelinePortal.Value.knobFrame;
|
||||
for (var i = 0; i < recordedData.Value.animHistory.Length; ++i)
|
||||
{
|
||||
var ah = recordedData.Value.animHistory[i];
|
||||
|
||||
if (knobFrame >= ah.frameSpan.x && knobFrame <= ah.frameSpan.y)
|
||||
{
|
||||
var animationWeight = AnimationHistoryData.GetHistoryValueForFrame(ah.historyWeights, knobFrame);
|
||||
var animationTime = AnimationHistoryData.GetHistoryValueForFrame(ah.historyAnimTime, knobFrame);
|
||||
var atp = new AnimationToProcessComponent()
|
||||
{
|
||||
animation = BlobDatabaseSingleton.GetBlobAsset(ah.animationHash, animDBSingleton.animations),
|
||||
avatarMask = BlobDatabaseSingleton.GetBlobAsset(ah.avatarMaskHash, animDBSingleton.avatarMasks),
|
||||
blendMode = ah.blendMode,
|
||||
layerIndex = ah.layerIndex,
|
||||
layerWeight = ah.layerWeight,
|
||||
motionId = ah.motionId,
|
||||
time = animationTime,
|
||||
weight = animationWeight
|
||||
};
|
||||
pc.ValueRW.playbackData.Add(atp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void StartPreview()
|
||||
{
|
||||
StopRecord();
|
||||
|
||||
var world = GetSelectedWorld();
|
||||
if (world == null || selectedEntity == Entity.Null)
|
||||
return;
|
||||
|
||||
var isPreview = IsPreview();
|
||||
Assert.IsFalse(isPreview);
|
||||
entityPath.SetEnabled(false);
|
||||
recordBtn.SetEnabled(false);
|
||||
previewBtn.style.color = new StyleColor(Color.cyan);
|
||||
|
||||
var pc = new PlaybackComponent() { playbackData = new (0xff, Allocator.Persistent) };
|
||||
world.EntityManager.AddComponentData(selectedEntity, pc);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void StopPreview()
|
||||
{
|
||||
previewBtn.style.color = StyleKeyword.Null;
|
||||
recordBtn.SetEnabled(true);
|
||||
entityPath.SetEnabled(true);
|
||||
|
||||
var isPreview = IsPreview();
|
||||
if (!isPreview)
|
||||
return;
|
||||
|
||||
var world = GetSelectedWorld();
|
||||
world.EntityManager.RemoveComponent<PlaybackComponent>(selectedEntity);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
unsafe bool IsRecording() => recordSingletonEq.__impl != null && recordSingletonEq.HasSingleton<RecordComponent>();
|
||||
unsafe bool IsPreview() => playbackSingletonEq.__impl != null && playbackSingletonEq.HasSingleton<PlaybackComponent>();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void SaveRecordedData()
|
||||
{
|
||||
var path = EditorUtility.SaveFilePanel("Rukhanka Wayback Machine", "", "WaybackMachineRecord", "rwbm");
|
||||
recordedData.Value.SerializeToFile(path);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
unsafe void LoadRecordedData()
|
||||
{
|
||||
var path = EditorUtility.OpenFilePanel("Rukhanka Wayback Machine", "", "rwbm");
|
||||
recordedData.GetUnsafePtr()->SerializeFromFile(path);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void DropDown(GenericDropdownMenu m, VisualElement targetElement)
|
||||
{
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
m.DropDown(entityPathEntity.worldBound, entityPathEntity, DropdownMenuSizeMode.Content);
|
||||
#else
|
||||
m.DropDown(targetElement.worldBound, targetElement, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
StopRecord();
|
||||
StopPreview();
|
||||
if (recordedData.IsCreated)
|
||||
recordedData.Value.Dispose();
|
||||
recordedData.Dispose();
|
||||
timelinePortal.Dispose();
|
||||
timelineContent.Dispose();
|
||||
DisposeEntityQueries();
|
||||
EditorApplication.playModeStateChanged -= playModeChangeFn;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user