Command pattern (Undo/Redo) system began. (Read desc)

I spent about 6 hours trying to fix this one specific bug involving the move undo. Turns out all I had to do was calm down and think logically instead of typing random bullshit for a few hours until it worked. I'm tired and I thank this for ruining my sleep schedule.
This commit is contained in:
Braedon
2022-01-22 05:44:19 -05:00
parent dea5860e95
commit 1ed2291844
26 changed files with 2206 additions and 77 deletions

View File

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

View File

@ -0,0 +1,75 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RhythmHeavenMania.Editor.Commands;
namespace RhythmHeavenMania.Editor
{
public class CommandManager : MonoBehaviour
{
private Stack<IAction> historyStack = new Stack<IAction>();
private Stack<IAction> redoHistoryStack = new Stack<IAction>();
int maxItems = 128;
public bool canUndo()
{
return historyStack.Count > 0;
}
public bool canRedo()
{
return redoHistoryStack.Count > 0;
}
public static CommandManager instance { get; private set; }
private void Awake()
{
instance = this;
}
public void Execute(IAction action)
{
action.Execute();
historyStack.Push(action);
redoHistoryStack.Clear();
}
public void Undo()
{
if (!canUndo()) return;
if (historyStack.Count > 0)
{
redoHistoryStack.Push(historyStack.Peek());
historyStack.Pop().Undo();
}
}
public void Redo()
{
if (!canRedo()) return;
if (redoHistoryStack.Count > 0)
{
historyStack.Push(redoHistoryStack.Peek());
redoHistoryStack.Pop().Redo();
}
}
// this is here as to not hog up memory, "max undos" basically
private void EnsureCapacity()
{
if (maxItems > 0)
{
}
}
private void Clear()
{
historyStack.Clear();
redoHistoryStack.Clear();
}
}
}

View File

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

View File

@ -0,0 +1,13 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RhythmHeavenMania.Editor.Commands
{
public interface IAction
{
void Execute();
void Undo();
void Redo();
}
}

View File

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

View File

@ -0,0 +1,129 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace RhythmHeavenMania.Editor.Commands
{
public class Selection : IAction
{
public void Execute()
{
throw new System.NotImplementedException();
}
public void Redo()
{
throw new System.NotImplementedException();
}
public void Undo()
{
throw new System.NotImplementedException();
}
}
// I spent 7 hours trying to fix this instead of sleeping, which would've probably worked better.
// I'll go fuck myself later I'm just glad it works
// I give massive props to people who code undo/redo systems
// -- Starpelly
public class Move : IAction
{
public List<Pos> pos = new List<Pos>();
public class Pos
{
public TimelineEventObj eventObj;
public Vector2 lastPos_;
public Vector3 previousPos;
}
public Move(List<TimelineEventObj> eventObjs)
{
pos.Clear();
for (int i = 0; i < eventObjs.Count; i++)
{
Pos p = new Pos();
p.eventObj = eventObjs[i];
p.lastPos_ = eventObjs[i].lastPos_;
p.previousPos = eventObjs[i].transform.localPosition;
this.pos.Add(p);
}
}
public void Execute()
{
}
public void Redo()
{
for (int i = 0; i < pos.Count; i++)
{
EnsureEventObj(i);
pos[i].eventObj.transform.localPosition = pos[i].previousPos;
}
}
public void Undo()
{
for (int i = 0; i < pos.Count; i++)
{
EnsureEventObj(i);
pos[i].eventObj.transform.localPosition = pos[i].lastPos_;
}
}
private void EnsureEventObj(int id)
{
if (pos[id].eventObj == null)
{
pos[id].eventObj = GameManager.instance.Beatmap.entities.Find(c => c.eventObj.eventObjID == pos[id].eventObj.eventObjID).eventObj;
}
}
}
public class Deletion : IAction
{
List<TimelineEventObj> eventObjs;
List<TimelineEventObj> deletedObjs;
public Deletion(List<TimelineEventObj> eventObjs)
{
this.eventObjs = eventObjs;
}
public void Execute()
{
deletedObjs = eventObjs;
for (int i = 0; i < eventObjs.Count; i++)
{
Selections.instance.Deselect(eventObjs[i]);
Timeline.instance.DestroyEventObject(eventObjs[i].entity);
}
}
public void Redo()
{
deletedObjs = eventObjs;
for (int i = 0; i < eventObjs.Count; i++)
{
Selections.instance.Deselect(eventObjs[i]);
Timeline.instance.DestroyEventObject(eventObjs[i].entity);
}
}
public void Undo()
{
for (int i = 0; i < deletedObjs.Count; i++)
{
Beatmap.Entity e = deletedObjs[i].entity;
eventObjs[i] = Timeline.instance.AddEventObject(e.datamodel, false, new Vector3(e.beat, -e.track * Timeline.instance.LayerHeight()), e, true, e.eventObj.eventObjID);
}
}
}
}

View File

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

View File

@ -0,0 +1,34 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RhythmHeavenMania.Editor.Commands;
public class TestCommand : IAction
{
private GameObject prefab;
private Vector3 pos;
private GameObject spawnedgameObj;
public TestCommand(GameObject prefab, Vector3 pos)
{
this.prefab = prefab;
this.pos = pos;
}
public void Execute()
{
spawnedgameObj = GameObject.Instantiate(prefab, pos, Quaternion.identity);
}
public void Redo()
{
throw new System.NotImplementedException();
}
public void Undo()
{
GameObject.Destroy(spawnedgameObj);
}
}

View File

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

View File

@ -6,6 +6,7 @@ using UnityEngine.UI;
using Newtonsoft.Json;
using TMPro;
using Starpelly;
namespace RhythmHeavenMania.Editor
{
@ -29,6 +30,8 @@ namespace RhythmHeavenMania.Editor
[SerializeField] private Button NewBTN;
[SerializeField] private Button OpenBTN;
[SerializeField] private Button SaveBTN;
[SerializeField] private Button UndoBTN;
[SerializeField] private Button RedoBTN;
public static List<TimelineEventObj> EventObjs = new List<TimelineEventObj>();
@ -60,6 +63,8 @@ namespace RhythmHeavenMania.Editor
Tooltip.AddTooltip(NewBTN.gameObject, "New");
Tooltip.AddTooltip(OpenBTN.gameObject, "Open");
Tooltip.AddTooltip(SaveBTN.gameObject, "Save");
Tooltip.AddTooltip(UndoBTN.gameObject, "Undo");
Tooltip.AddTooltip(RedoBTN.gameObject, "Redo");
}
public void Update()
@ -77,6 +82,40 @@ namespace RhythmHeavenMania.Editor
GetComponent<Selector>().enabled = true;
GetComponent<BoxSelection>().enabled = true;
}*/
if (Input.GetKeyDown(KeyCode.Delete))
{
List<TimelineEventObj> ev = new List<TimelineEventObj>();
for (int i = 0; i < Selections.instance.eventsSelected.Count; i++) ev.Add(Selections.instance.eventsSelected[i]);
CommandManager.instance.Execute(new Commands.Deletion(ev));
}
if (CommandManager.instance.canUndo())
UndoBTN.transform.GetChild(0).GetComponent<Image>().color = "BE72FF".Hex2RGB();
else
UndoBTN.transform.GetChild(0).GetComponent<Image>().color = Color.gray;
if (CommandManager.instance.canRedo())
RedoBTN.transform.GetChild(0).GetComponent<Image>().color = "7299FF".Hex2RGB();
else
RedoBTN.transform.GetChild(0).GetComponent<Image>().color = Color.gray;
if (Input.GetMouseButtonUp(0) && Timeline.instance.CheckIfMouseInTimeline())
{
List<TimelineEventObj> selectedEvents = Timeline.instance.eventObjs.FindAll(c => c.selected == true && c.eligibleToMove == true);
if (selectedEvents.Count > 0)
{
List<TimelineEventObj> result = new List<TimelineEventObj>();
for (int i = 0; i < selectedEvents.Count; i++)
{
result.Add(selectedEvents[i]);
selectedEvents[i].OnUp();
}
CommandManager.instance.Execute(new Commands.Move(result));
}
}
}
public static Sprite GameIcon(string name)

View File

@ -161,11 +161,11 @@ namespace RhythmHeavenMania.Editor
dragTimes++;
if (currentEventIndex == 0)
{
Timeline.instance.AddEventObject($"gameManager/switchGame/{mg.name}", true, new Vector3(0, 0));
Timeline.instance.AddEventObject($"gameManager/switchGame/{mg.name}", true, new Vector3(0, 0), null, true);
}
else
{
Timeline.instance.AddEventObject(mg.name + "/" + mg.actions[currentEventIndex - 1].actionName, true, new Vector3(0, 0));
Timeline.instance.AddEventObject(mg.name + "/" + mg.actions[currentEventIndex - 1].actionName, true, new Vector3(0, 0), null, true);
}
}
}

View File

@ -1,6 +1,7 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
namespace RhythmHeavenMania.Editor
{
@ -14,10 +15,5 @@ namespace RhythmHeavenMania.Editor
{
instance = this;
}
private void Update()
{
}
}
}

View File

@ -51,7 +51,7 @@ namespace RhythmHeavenMania.Editor
var entity = GameManager.instance.Beatmap.entities[i];
var e = GameManager.instance.Beatmap.entities[i];
AddEventObject(e.datamodel, false, new Vector3(e.beat, -e.track * LayerHeight()), i);
AddEventObject(e.datamodel, false, new Vector3(e.beat, -e.track * LayerHeight()), e, false, Starpelly.Random.Strings.RandomString(Starpelly.Enums.Strings.StringType.Alphanumeric, 128));
}
TimelineSlider.GetChild(0).GetComponent<Image>().color = EditorTheme.theme.properties.BeatMarkerCol.Hex2RGB();
@ -296,7 +296,7 @@ namespace RhythmHeavenMania.Editor
#region Functions
public void AddEventObject(string eventName, bool dragNDrop = false, Vector3 pos = new Vector3(), int entityId = 0)
public TimelineEventObj AddEventObject(string eventName, bool dragNDrop = false, Vector3 pos = new Vector3(), Beatmap.Entity entity = null, bool addEvent = false, string eventId = "")
{
GameObject g = Instantiate(TimelineEventObjRef.gameObject, TimelineEventObjRef.parent);
g.transform.localPosition = pos;
@ -322,9 +322,9 @@ namespace RhythmHeavenMania.Editor
else
{
eventObj.resizable = true;
if (gameAction.defaultLength != GameManager.instance.Beatmap.entities[entityId].length && dragNDrop == false)
if (gameAction.defaultLength != entity.length && dragNDrop == false)
{
g.GetComponent<RectTransform>().sizeDelta = new Vector2(GameManager.instance.Beatmap.entities[entityId].length, LayerHeight());
g.GetComponent<RectTransform>().sizeDelta = new Vector2(entity.length, LayerHeight());
}
else
{
@ -340,32 +340,46 @@ namespace RhythmHeavenMania.Editor
var mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
g.transform.position = new Vector3(mousePos.x, mousePos.y, 0);
Beatmap.Entity en = new Beatmap.Entity();
en.datamodel = eventName;
en.eventObj = eventObj;
GameManager.instance.Beatmap.entities.Add(en);
GameManager.instance.SortEventsList();
Selections.instance.ClickSelect(eventObj);
eventObj.moving = true;
}
else
{
var entity = GameManager.instance.Beatmap.entities[entityId];
entity.eventObj = g.GetComponent<TimelineEventObj>();
entity.track = (int)(g.transform.localPosition.y / LayerHeight() * -1);
}
if (addEvent)
{
if (entity == null)
{
Beatmap.Entity en = new Beatmap.Entity();
en.datamodel = eventName;
en.eventObj = eventObj;
GameManager.instance.Beatmap.entities.Add(en);
GameManager.instance.SortEventsList();
}
else
{
GameManager.instance.Beatmap.entities.Add(entity);
GameManager.instance.SortEventsList();
}
}
Editor.EventObjs.Add(eventObj);
eventObjs.Add(eventObj);
eventObj.eventObjID = eventId;
return eventObj;
}
public void DestroyEventObject(Beatmap.Entity entity)
{
Editor.EventObjs.Remove(entity.eventObj);
GameManager.instance.Beatmap.entities.Remove(entity);
Timeline.instance.eventObjs.Remove(entity.eventObj);
Destroy(entity.eventObj.gameObject);
GameManager.instance.SortEventsList();

View File

@ -14,6 +14,7 @@ namespace RhythmHeavenMania.Editor
private float startPosY;
private Vector3 lastPos;
public Vector2 lastPos_;
private RectTransform rectTransform;
[Header("Components")]
@ -27,9 +28,9 @@ namespace RhythmHeavenMania.Editor
[SerializeField] private RectTransform rightDrag;
[Header("Properties")]
private Beatmap.Entity entity;
public Beatmap.Entity entity;
public float length;
private bool eligibleToMove = false;
public bool eligibleToMove = false;
private bool lastVisible;
public bool selected;
public bool mouseHovering;
@ -39,18 +40,24 @@ namespace RhythmHeavenMania.Editor
private bool resizingLeft;
private bool resizingRight;
private bool inResizeRegion;
public Vector2 lastMovePos;
public string eventObjID;
[Header("Colors")]
public Color NormalCol;
private void Start()
{
lastPos_ = transform.localPosition;
rectTransform = GetComponent<RectTransform>();
if (!resizable)
{
Destroy(resizeGraphic.gameObject);
}
lastMovePos = transform.localPosition;
}
private void Update()
@ -89,8 +96,8 @@ namespace RhythmHeavenMania.Editor
{
if (Input.GetKeyDown(KeyCode.Delete))
{
Selections.instance.Deselect(this);
Timeline.instance.DestroyEventObject(entity);
/*Selections.instance.Deselect(this);
Timeline.instance.DestroyEventObject(entity);*/
}
selectedImage.gameObject.SetActive(true);
@ -119,18 +126,22 @@ namespace RhythmHeavenMania.Editor
}
}
OnUp();
// OnUp();
}
if (Timeline.instance.eventObjs.FindAll(c => c.moving).Count > 0 && selected)
{
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
// lastPos_ = transform.localPosition;
this.transform.position = new Vector3(mousePos.x - startPosX, mousePos.y - startPosY - 0.40f, 0);
this.transform.localPosition = new Vector3(Mathf.Clamp(Mathp.Round2Nearest(this.transform.localPosition.x, 0.25f), 0, Mathf.Infinity), Timeline.instance.SnapToLayer(this.transform.localPosition.y));
if (lastPos != transform.localPosition)
{
OnMove();
}
lastPos = this.transform.localPosition;
}
@ -148,7 +159,7 @@ namespace RhythmHeavenMania.Editor
rectTransform.sizeDelta = new Vector2(Mathp.Round2Nearest(sizeDelta.x, 0.25f), sizeDelta.y);
SetPivot(new Vector2(0, rectTransform.pivot.y));
OnComplete();
OnComplete(false);
}
else if (resizingRight)
{
@ -162,7 +173,7 @@ namespace RhythmHeavenMania.Editor
rectTransform.sizeDelta = new Vector2(Mathp.Round2Nearest(sizeDelta.x, 0.25f), sizeDelta.y);
SetPivot(new Vector2(0, rectTransform.pivot.y));
OnComplete();
OnComplete(false);
}
if (Input.GetMouseButtonUp(0))
@ -203,6 +214,8 @@ namespace RhythmHeavenMania.Editor
{
if (selected)
{
lastPos_ = transform.localPosition;
for (int i = 0; i < Timeline.instance.eventObjs.Count; i++)
{
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
@ -211,22 +224,26 @@ namespace RhythmHeavenMania.Editor
}
moving = true;
OnComplete();
// lastMovePos = transform.localPosition;
// OnComplete();
}
}
public void OnUp()
{
// lastPos_ = this.lastPos_;
// previousPos = this.transform.localPosition;
if (selected)
{
moving = false;
if (eligibleToMove)
{
OnComplete();
OnComplete(true);
}
moving = false;
Cancel();
}
}
@ -320,10 +337,10 @@ namespace RhythmHeavenMania.Editor
eligibleToMove = true;
}
OnComplete();
OnComplete(true);
}
private void OnComplete()
private void OnComplete(bool move)
{
entity.length = rectTransform.sizeDelta.x;
entity.beat = this.transform.localPosition.x;