diff --git a/Changelog.md b/Changelog.md index 6984579..2a6d079 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,77 @@ --- +## In Progress (2.1) + +### Added + +- **Remember last state**: This is a new parameter in the constructor of the `StateMachine` class that is interesting for nested state machines. When set to true, it makes the state machine return to its last active state when it enters, instead of its original start state. You can also use this feature in the `HybridStateMachine` class. + +- **Run states in parallel**: The new `ParallelStates` class allows you to run multiple states in parallel. If `needsExitTime` is set to true, it will wait until any one of the child states calls `StateCanExit` before it exits. This behaviour can be overridden by providing a custom `canExit` function. + + E.g. + ```csharp + var attackFsm = new StateMachine(); + attackFsm.AddState("Idle"); + attackFsm.AddState("Attack"); + // ... + + fsm.AddState("A", new ParallelStates( + new State(onLogic: s => MoveTowardsPlayer()), + new State(onLogic: s => Animate()), + attackFsm + )); + ``` + + With a custom `canExit` function: + + ```csharp + fsm.AddState("A", new ParallelStates( + canExit: s => IsPlayerInRange(), + needsExitTime: true, + + new State(onLogic: s => MoveTowardsPlayer()), + new State(onLogic: s => Animate()) + )); + ``` + +- **Active State Changed Event**: The `StateMachine` class now has a new event that you can subscribe to that is triggered when its active state is changed: + + E.g. + ```csharp + fsm.StateChanged += state => print(state.name); + + fsm.AddState("A"); + fsm.AddState("B"); + fsm.AddTransition("A", "B"); + + fsm.Init(); // prints "A" + + fsm.OnLogic(); // prints "B" + ``` + +### Improved + +- Improved the performance of the `OnLogic` and the `Trigger` methods of the `StateMachine` class when states have multiple outgoing transitions. Depending on the number of transitions, when using string state names, this can make the `OnLogic` method up to 15% faster. + +- The naming of the key / mouse transition classes has been improved by following the C# naming convention for events. + - `TransitionOnKey.Press` is now `TransitionOnKey.Pressed` + - `TransitionOnKey.Release` is now `TransitionOnKey.Released` + - `TransitionOnMouse.Press` is now `TransitionOnMouse.Pressed` + - `TransitionOnMouse.Release` is now `TransitionOnMouse.Released` + +- Improved documentation. + +### Fixed + +- Fix incorrect execution order (timing) bug concerning the `canExit` feature of the `State` class. + +- Fix `Time.time` access exception bug that occurred during the deserialisation of `State` and `State`-derived classes shown in the inspector. + +- Fix incorrect output of `GetActiveHierarchyPath()` in the `StateWrapper.WrappedState` class. + +--- + ## 2.0.1 ### Fixed @@ -17,53 +88,53 @@ ### Added - **Ghost states**: Ghost states are states that the state machine does not want to remain in and will try to exit as soon as possible. This means that the fsm can do multiple transitions in one `OnLogic` call. The "ghost state behaviour" is supported by all state types by setting the `isGhostState` field. - + E.g. - + ```csharp fsm.AddState("A", onEnter: s => print("A")); fsm.AddState("B", new State(onEnter: s => print("B"), isGhostState: true)); fsm.AddState("C", onEnter: s => print("C"); - + fsm.AddTransition("A", "B"); fsm.AddTransition("B", "C"); - + fsm.Init(); // Prints "A" fsm.OnLogic(); // Prints "B" and then "C" ``` - **Exit transitions**: Exit transitions finally provide an easy and powerful way to define the exit conditions for nested state machines, essentially levelling up the mechanics behind hierarchical state machines. Previously, the rule that determined when a nested state machine that `needsExitTime` can exit, was implicit, not versatile, and not in the control of the developer. - + ```csharp var nested = new StateMachine(needsExitTime: true); nested.AddState("A"); nested.AddState("B"); // ... - + // The nested fsm can only exit when it is in the "B" state and // the variable x equals 0. move.AddExitTransition("B", t => x == 0); ``` - + Exit transitions can also be defined for all states (`AddExitTransitionFromAny`), as trigger transitions (`AddExitTriggerTransition`), or as both (`AddExitTriggerTransitionFromAny`). - **Transition callbacks**: New feature that lets you define a function that is called when a transition succeeds. It is supported by all transition types (e.g. trigger transitions, transitions from any, exit transitions, ...). - + ```csharp fsm.AddTransition( new Transition("A", "B", onTransition: t => print("Transition")) ); ``` - + This feature is also supported when using the shortcut methods: - + ```csharp // Can be shortened using shortcut methods: fsm.AddTransition("A", "B", onTransition: t => print("Transition")); ``` - + The print function will be called just before the transition. You can also define a callback that is called just after the transition: - + ```csharp fsm.AddTransition("A", "B", onTransition: t => print("Before"), @@ -72,18 +143,18 @@ ``` - Support for **custom actions** in `HybridStateMachine`, just like in the normal `State` class: - + ```csharp var hybrid = new HybridStateMachine(); hybrid.AddState("A", new State().AddAction("Action", () => print("A"))); hybrid.AddAction("Action", () => print("Hybrid")); - + hybrid.Init(); hybrid.OnAction("Action"); // Prints "Hybrid" and then "A" ``` - Option in `HybridStateMachine` to **run custom code before and after** the `OnEnter` / `OnLogic` / ... of its active sub state. Previously, you could only add a custom callback that was run *after* the respective methods of the sub state. When migrating to this version simply replace the `onEnter` parameter with `afterOnEnter` in the constructor. For example - + ```csharp var hybrid = new HybridStateMachine( beforeOnEnter: fsm => print("Before OnEnter"), @@ -93,28 +164,28 @@ ``` - Feature for getting the **active path in the state hierarchy**: When debugging it is often useful to not only see what the active state of the root state machine is (using `ActiveStateName`) but also which state is active in any nested state machine. This path of states can now be retrieved using the new `GetActiveHierarchyPath()` method: - + ```csharp var fsm = new StateMachine(); var move = new StateMachine(); var jump = new StateMachine(); - + fsm.AddState("Move", move); move.AddState("Jump", jump); jump.AddState("Falling"); - + fsm.Init(); print(fsm.GetActiveHierarchyPath()); // Prints "/Move/Jump/Falling" ``` - Option in `CoState` to **only run the coroutine once**. E.g. - + ```csharp var state = new CoState(mono, myCoroutine, loop: false); ``` - Option in `TransitionAfterDynamic` to only evaluate the dynamic delay when the `from` state enters. This is useful, e.g. when the delay of a transition should be random. E.g. - + ```csharp fsm.AddTransition(new TransitionAfterDynamic( "A", "B", t => Random.Range(2, 10), onlyEvaluateDelayOnEnter: true @@ -148,40 +219,40 @@ ### Added - Action system to allow for adding and calling custom functions apart from `OnLogic`. - + E.g. - + ```csharp var state = new State() .AddAction("OnGameOver", () => print("Good game")) .AddAction("OnCollision", collision => print(collision)); - + fsm.AddState("State", state); fsm.Init(); - + fsm.OnAction("OnGameOver"); // prints "Good game" fsm.OnAction("OnCollision", new Collision2D()); ``` - Two way transitions: New feature that lets the state machine transition from a source to a target state when a condition is true, and from the target to the source state when the condition is false: - + ```csharp fsm.AddTwoWayTransition("Idle", "Shoot", t => isInRange); - + // Same as fsm.AddTransition("Idle", "Shoot", t => isInRange); fsm.AddTransition("Shoot", "Idle", t => ! isInRange); ``` - + ```csharp fsm.AddTwoWayTransition(transition); fsm.AddTwoWayTriggerTransition(transition); ``` - `TransitionOnMouse` classes for readable transitions that should occur when a certain mouse button has been pressed / released / ... It is analogous to `TransitionOnKey`. - + E.g.: - + ```csharp fsm.AddTransition(new TransitionOnMouse.Down("Idle", "Shoot", 0)); ``` @@ -197,36 +268,36 @@ - The "shortcut methods" of the state machine have been moved to a dedicated class as extension methods. This does not change the API or usage in any way, but makes the internal code cleaner. -> This change reduces the coupling between the base StateMachine class and the State / Transition classes. Instead, the StateMachine only depends on the StateBase and TransitionBase classes. This especially shows that the extension methods are optional and not necessary in a fundamental way. - To allow for better testing and more customisation, references to the Timer class have been replaced with the ITimer interface. This allows you to write a custom timer for your use case and allows for time-based transitions to be tested more easily. - + ```csharp // Previously if (timer > 2) { } - + // Now if (timer.Elapsed > 2) { } ``` - As a consequence of the way the action system was implemented, generic datatype of the input parameter of `onEnter` / `onLogic` / `onExit` for `State` and `CoState` has changed. The class `State` now requires two generic type parameters: One for the type of its ID and one for the type of the IDs of the actions. - + Previously: - + ```csharp void FollowPlayer(State state) { // ... } - + fsm.AddState("FollowPlayer", onLogic: FollowPlayer); ``` - + Now: - + ```csharp void FollowPlayer(State state) { // ... } - + fsm.AddState("FollowPlayer", onLogic: FollowPlayer); ``` @@ -247,7 +318,7 @@ Version 1.8 of UnityHFSM adds support for generics. Now the datatype of state id - Support for generics for the state identifiers and event names - "Shortcut methods" for reduced boilerplate and automatic optimisation - + ```csharp fsm.AddState("FollowPlayer", new State( onLogic: s => MoveTowardsPlayer() @@ -255,13 +326,13 @@ Version 1.8 of UnityHFSM adds support for generics. Now the datatype of state id // Now fsm.AddState("FollowPlayer", onLogic: s => MoveTowardsPlayer()); ``` - + ```csharp fsm.AddState("ExtractIntel", new State()); // Now fsm.AddState("ExtractIntel"); ``` - + ```csharp fsm.AddTransition(new Transition("A", "B")); // Now @@ -275,33 +346,33 @@ Version 1.8 of UnityHFSM adds support for generics. Now the datatype of state id ### Changed - The datatype of the input parameter of `onEnter` / `onLogic` / `onExit` for `State` has changed. This is due to the inheritance hierarchy and the way generic support was added to the codebase while still trying to retain the ease of use of the string versions. - + Previously: - + ```csharp void FollowPlayer(State state) { // ... } - + fsm.AddState("FollowPlayer", new State(onLogic: FollowPlayer)); ``` - + Now: - + ```csharp void FollowPlayer(State state) { // ... } - + fsm.AddState("FollowPlayer", new State(onLogic: FollowPlayer)); ``` - States and transitions no longer carry a reference to the MonoBehaviour by default. - + - Now the constructor of `StateMachine` does not require mono anymore => `new StateMachine()` instead of `new StateMachine(this)` - + - The reference to mono has to be passed into the `CoState` constructor => `new CoState(this, ...)` ### Fixed diff --git a/README.md b/README.md index f289922..e5bef6d 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ You can also add it directly from GitHub on Unity 2019.4+. Note that you won't b - Select Add from Git URL - Paste - `https://github.com/Inspiaaa/UnityHFSM.git#upm` for the latest stable release (**recommended**) - - `https://github.com/Inspiaaa/UnityHFSM.git` for the development version + - `https://github.com/Inspiaaa/UnityHFSM.git#release` for the development version - `https://github.com/Inspiaaa/UnityHFSM.git#v1.8.0` for a specific version (`v1.8.0` here) - Click Add - Tip: If you're using VSCode and you're not getting any IntelliSense, you may have to regenerate the `.csproj` project files (Edit > Preferences > External Tools > Regenerate project files) @@ -258,6 +258,8 @@ fsm.AddState("FollowPlayer", onLogic: state => MoveTowardsPlayer(1)); Although this example is using lambda expressions for the states' logic, you can of course also just pass normal functions. +> **Side note:** To keep things simple, we're using strings for the state identifiers. Just keep in mind that UnityHFSM is not limited to this, as it allows you to use any custom type (e.g. enums) for the state identifiers. See the [generics](#generics) chapter for more information. + #### Adding transitions ```csharp diff --git a/Tests/TestActiveStateChangedEvent.cs b/Tests/TestActiveStateChangedEvent.cs index bd48603..b88124f 100644 --- a/Tests/TestActiveStateChangedEvent.cs +++ b/Tests/TestActiveStateChangedEvent.cs @@ -1,31 +1,31 @@ using NUnit.Framework; -using FSM; +using UnityHFSM; using System; using System.Collections.Generic; using System.Linq; -namespace FSM.Tests +namespace UnityHFSM.Tests { public class TestActiveStateChangedEvent { private StateMachine fsm; - private List trackedStates; + private Recorder recorder; [SetUp] public void Setup() { fsm = new StateMachine(); - trackedStates = new List(); + recorder = new Recorder(); } [Test] public void Test_active_state_changed_event() { - fsm.OnActiveStateChanged += state => trackedStates.Add(state != null ? state.name : "null"); + fsm.StateChanged += state => recorder.RecordCustom($"StateChanged({state.name})"); - fsm.AddState("A", new State()); - fsm.AddState("B", new State()); - fsm.AddState("C", new State()); + fsm.AddState("A", recorder.TrackedState); + fsm.AddState("B", recorder.TrackedState); + fsm.AddState("C", recorder.TrackedState); fsm.AddTransition("A", "B"); fsm.AddTransition("B", "C"); @@ -33,24 +33,56 @@ public void Test_active_state_changed_event() fsm.SetStartState("A"); fsm.Init(); - AssertTrackedStated(expected: new[] { "A" }); + recorder.Expect + .Enter("A") + .Custom("StateChanged(A)") + .All(); fsm.OnLogic(); - AssertTrackedStated(expected: new[] { "A", "B" }); + recorder.Expect + .Exit("A") + .Enter("B") + .Custom("StateChanged(B)") + .Logic("B") + .All(); fsm.OnLogic(); - AssertTrackedStated(expected: new[] { "A", "B", "C" }); + recorder.Expect + .Exit("B") + .Enter("C") + .Custom("StateChanged(C)") + .Logic("C") + .All(); fsm.OnExit(); - AssertTrackedStated(expected: new[] { "A", "B", "C", "null" }); + recorder.Expect + .Exit("C") + .All(); } - private void AssertTrackedStated(IEnumerable expected) + [Test] + public void Test_active_state_changed_event_works_with_ghost_states() { - if (!trackedStates.SequenceEqual(expected)) - { - Assert.Fail($"Tracked active states is not equals with expected. Real: ({string.Join(',', trackedStates)}), Expected : ({string.Join(',', expected)})"); - } + fsm.StateChanged += state => recorder.RecordCustom($"StateChanged({state.name})"); + + fsm.AddState("A", recorder.Track(new State(isGhostState: true))); + fsm.AddState("B", recorder.Track(new State(isGhostState: true))); + fsm.AddState("C", recorder.Track(new State(isGhostState: true))); + + fsm.AddTransition("A", "B"); + fsm.AddTransition("B", "C"); + + fsm.Init(); + recorder.Expect + .Enter("A") + .Custom("StateChanged(A)") + .Exit("A") + .Enter("B") + .Custom("StateChanged(B)") + .Exit("B") + .Enter("C") + .Custom("StateChanged(C)") + .All(); } } } \ No newline at end of file diff --git a/Tests/TestCanExit.cs b/Tests/TestCanExit.cs index d9cf6bf..0c8f265 100644 --- a/Tests/TestCanExit.cs +++ b/Tests/TestCanExit.cs @@ -57,5 +57,40 @@ public void Test_state_with_needsExitTime_can_exit_later_when_canExit_switches_t fsm.OnLogic(); Assert.AreEqual("B", fsm.ActiveStateName); } + + [Test] + public void Test_state_with_needsExitTime_calls_onLogic_before_transitioning_on_delayed_transition() + { + var canExit = false; + + var recorder = new Recorder(); + + fsm.AddState("A", recorder.Track(new State( + onLogic: state => recorder.RecordCustom("UserOnLogic"), + needsExitTime: true, + canExit: state => canExit))); + fsm.AddState("B", recorder.TrackedState); + + fsm.Init(); + fsm.OnLogic(); + + fsm.RequestStateChange("B"); + Assert.AreEqual("A", fsm.ActiveStateName); + + recorder.DiscardAll(); + + canExit = true; + fsm.OnLogic(); + + recorder.Expect + .Logic("A") + .Custom("UserOnLogic") + .Exit("A") + .Enter("B") + .All(); + + fsm.OnLogic(); + recorder.Expect.Logic("B").All(); + } } } diff --git a/Tests/TestParallelStates.cs b/Tests/TestParallelStates.cs new file mode 100644 index 0000000..87623c9 --- /dev/null +++ b/Tests/TestParallelStates.cs @@ -0,0 +1,291 @@ +using NUnit.Framework; +using UnityHFSM; +using System; + +namespace UnityHFSM.Tests +{ + public class TestParallelStates + { + private Recorder recorder; + private StateMachine fsm; + + [SetUp] + public void Setup() + { + recorder = new Recorder(); + fsm = new StateMachine(); + } + + [Test] + public void Test_ps_sets_up_implicit_names_for_string_states() + { + var stateA = new State(); + var stateB = new State(); + var stateC = new State(); + + var ps = new ParallelStates(stateA, stateB, stateC); + Assert.AreEqual("0", stateA.name); + Assert.AreEqual("1", stateB.name); + Assert.AreEqual("2", stateC.name); + } + + [Test] + public void Test_ps_calls_OnEnter_OnLogic_OnExit_on_child_states() + { + fsm.AddState("Start", new ParallelStates() + .AddState("A", recorder.TrackedState) + .AddState("B", recorder.TrackedState) + ); + fsm.AddState("Other", recorder.TrackedState); + + fsm.Init(); + recorder.Expect + .Enter("A") + .Enter("B") + .All(); + + fsm.OnLogic(); + recorder.Expect + .Logic("A") + .Logic("B") + .All(); + + fsm.RequestStateChange("Other"); + recorder.Expect + .Exit("A") + .Exit("B") + .Enter("Other") + .All(); + } + + [Test] + public void Test_ps_exits_instantly_when_needsExitTime_is_false() + { + fsm.AddState("A", new ParallelStates(needsExitTime: false, new State(needsExitTime: true))); + fsm.AddState("B"); + fsm.AddTransition("A", "B"); + + fsm.Init(); + Assert.AreEqual("A", fsm.ActiveStateName); + fsm.OnLogic(); + Assert.AreEqual("B", fsm.ActiveStateName); + } + + [Test] + public void Test_ps_exits_instantly_on_request_when_any_child_state_can_exit() + { + bool firstCanExit = false; + bool secondCanExit = false; + + fsm.AddState("A", new ParallelStates(needsExitTime: true, + new State(needsExitTime: true, canExit: state => firstCanExit), + new State(needsExitTime: true, canExit: state => secondCanExit) + )); + + fsm.AddState("B"); + + fsm.Init(); + Assert.AreEqual("A", fsm.ActiveStateName); + + firstCanExit = true; + fsm.RequestStateChange("B"); + Assert.AreEqual("B", fsm.ActiveStateName); + + firstCanExit = false; + fsm.RequestStateChange("A"); + Assert.AreEqual("A", fsm.ActiveStateName); + + secondCanExit = true; + fsm.RequestStateChange("B"); + Assert.AreEqual("B", fsm.ActiveStateName); + } + + [Test] + public void Test_ps_exits_later_when_any_child_state_can_exit() + { + bool firstCanExit = false; + bool secondCanExit = false; + + fsm.AddState("A", new ParallelStates(needsExitTime: true, + new State(needsExitTime: true, canExit: state => firstCanExit), + new State(needsExitTime: true, canExit: state => secondCanExit) + )); + + fsm.AddState("B"); + + fsm.Init(); + fsm.RequestStateChange("B"); + fsm.OnLogic(); + fsm.OnLogic(); + Assert.AreEqual("A", fsm.ActiveStateName); + + firstCanExit = true; + fsm.OnLogic(); + fsm.OnLogic(); + Assert.AreEqual("B", fsm.ActiveStateName); + + firstCanExit = false; + fsm.RequestStateChange("A"); + Assert.AreEqual("A", fsm.ActiveStateName); + + fsm.RequestStateChange("B"); + secondCanExit = true; + fsm.OnLogic(); + Assert.AreEqual("B", fsm.ActiveStateName); + } + + [Test] + public void Test_ps_exits_on_exit_transition_of_child_fsm() + { + var nestedFsm = new StateMachine(needsExitTime: true); + nestedFsm.AddState("Start", needsExitTime: true); + nestedFsm.AddExitTransition("Start"); + + fsm.AddState("A", new ParallelStates( + needsExitTime: true, + new State(needsExitTime: true), + nestedFsm + )); + fsm.AddState("B"); + fsm.AddTransition("A", "B"); + + fsm.Init(); + fsm.OnLogic(); + Assert.AreEqual("A", fsm.ActiveStateName); + + nestedFsm.StateCanExit(); + Assert.AreEqual("B", fsm.ActiveStateName); + } + + [Test] + public void Test_ps_with_exit_time_exits_instantly_when_canExit_returns_true() + { + fsm.AddState("A", new ParallelStates( + canExit: state => true, + needsExitTime: true, + new State(needsExitTime: true))); + fsm.AddState("B"); + + fsm.Init(); + Assert.AreEqual("A", fsm.ActiveStateName); + fsm.RequestStateChange("B"); + Assert.AreEqual("B", fsm.ActiveStateName); + } + + [Test] + public void Test_ps_with_exit_time_exits_as_soon_as_canExit_returns_true() + { + bool canExit = false; + fsm.AddState("A", new ParallelStates( + canExit: state => canExit, + needsExitTime: true, + new State(needsExitTime: true))); + fsm.AddState("B"); + + fsm.Init(); + + fsm.RequestStateChange("B"); + + fsm.OnLogic(); + Assert.AreEqual("A", fsm.ActiveStateName); + + canExit = true; + fsm.OnLogic(); + Assert.AreEqual("B", fsm.ActiveStateName); + } + + [Test] + public void Test_ps_ignores_child_StateCanExit_calls_when_canExit_is_defined() + { + bool canExit = false; + fsm.AddState("A", new ParallelStates( + canExit: state => canExit, + needsExitTime: true, + new State(canExit: s => true, needsExitTime: true))); + + fsm.AddTransition("A", "B"); + + fsm.AddState("B"); + + fsm.Init(); + + fsm.RequestStateChange("B"); + Assert.AreEqual("A", fsm.ActiveStateName); + + fsm.OnLogic(); + Assert.AreEqual("A", fsm.ActiveStateName); + + canExit = true; + fsm.OnLogic(); + Assert.AreEqual("B", fsm.ActiveStateName); + } + + [Test] + public void Test_ps_active_hierarchy_path_for_named_states() + { + var nestedFsm = new StateMachine(); + nestedFsm.AddState("D"); + fsm.AddState("PS", new ParallelStates() + .AddState("A", new State()) + .AddState("B", new State()) + .AddState("C", nestedFsm) + ); + + fsm.Init(); + Assert.AreEqual("/PS/(A & B & C/D)", fsm.GetActiveHierarchyPath()); + } + + [Test] + public void Test_ps_active_hierarchy_path_for_single_child_state() + { + fsm.AddState("PS", new ParallelStates() + .AddState("A", new State()) + ); + + fsm.Init(); + Assert.AreEqual("/PS/A", fsm.GetActiveHierarchyPath()); + } + + [Test] + public void Test_ps_active_hierarchy_path_for_no_child_states() + { + fsm.AddState("PS", new ParallelStates()); + + fsm.Init(); + Assert.AreEqual("/PS", fsm.GetActiveHierarchyPath()); + } + + [Test] + public void Test_ps_active_hierarchy_path_for_nameless_states() + { + fsm.AddState("PS", new ParallelStates( + new State(), new State() + )); + + fsm.Init(); + Assert.AreEqual("/PS", fsm.GetActiveHierarchyPath()); + } + + [Test] + public void Test_ps_active_hierarchy_path_does_not_throw_error_when_root() + { + var root = new ParallelStates() + .AddState("A", new State()) + .AddState("B", new State()); + + Assert.DoesNotThrow(() => root.GetActiveHierarchyPath()); + } + + [Test] + public void Test_ps_child_state_can_use_different_type_for_id() + { + var ps = new ParallelStates(); + ps.AddState(0, new State()); + ps.AddState(1, new State()); + + fsm.AddState("A", ps); + fsm.Init(); + fsm.OnLogic(); + } + } +} \ No newline at end of file diff --git a/Tests/TestParallelStates.cs.meta b/Tests/TestParallelStates.cs.meta new file mode 100644 index 0000000..05348b5 --- /dev/null +++ b/Tests/TestParallelStates.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 55ac25ac7cb58aa4fbb84bb3dec1ea1f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/TestRememberLastState.cs b/Tests/TestRememberLastState.cs new file mode 100644 index 0000000..f4c509c --- /dev/null +++ b/Tests/TestRememberLastState.cs @@ -0,0 +1,77 @@ +using NUnit.Framework; +using UnityHFSM; +using System; + +namespace UnityHFSM.Tests +{ + public class TestRememberLastState + { + private StateMachine fsm; + + [SetUp] + public void Setup() + { + fsm = new StateMachine(); + } + + private void AssertStatesAreActive(string activeHierarchyPath) + { + Assert.AreEqual(activeHierarchyPath, fsm.GetActiveHierarchyPath()); + } + + [Test] + public void Test_nested_fsm_returns_to_start_state_by_default_on_enter() + { + var nested = new StateMachine(); + nested.AddState("X"); + nested.AddState("Y"); + nested.SetStartState("Y"); + + fsm.AddState("A", nested); + fsm.AddState("B"); + + fsm.Init(); + AssertStatesAreActive("/A/Y"); + + nested.RequestStateChange("X"); + AssertStatesAreActive("/A/X"); + + fsm.RequestStateChange("B"); + AssertStatesAreActive("/B"); + + fsm.RequestStateChange("A"); + AssertStatesAreActive("/A/Y"); // Y is the default start state. + } + + [Test] + public void Test_remember_last_state_works() + { + var nested = new StateMachine(rememberLastState: true); + nested.AddState("X"); + nested.AddState("Y"); + nested.AddState("Z"); + nested.SetStartState("X"); + + nested.AddTransition("X", "Y"); + nested.AddTransition("Y", "Z"); + + fsm.AddState("A", nested); + fsm.AddState("B"); + + fsm.Init(); + AssertStatesAreActive("/A/X"); + + fsm.OnLogic(); + fsm.OnLogic(); + AssertStatesAreActive("/A/Z"); + + // Here A (the nested fsm) exits. This normally means that when it enters again, + // it will enter in its original start state (X). + fsm.RequestStateChange("B"); + AssertStatesAreActive("/B"); + + fsm.RequestStateChange("A"); + AssertStatesAreActive("/A/Z"); // Z is the remembered last state. + } + } +} diff --git a/Tests/TestRememberLastState.cs.meta b/Tests/TestRememberLastState.cs.meta new file mode 100644 index 0000000..1ac5ff3 --- /dev/null +++ b/Tests/TestRememberLastState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b06d5499e93920a4b811c3feb3a9f267 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/TestTwoWayTransitions.cs b/Tests/TestTwoWayTransitions.cs index eb29ca8..0b3c85a 100644 --- a/Tests/TestTwoWayTransitions.cs +++ b/Tests/TestTwoWayTransitions.cs @@ -3,7 +3,7 @@ namespace UnityHFSM.Tests { - public class TesTwoWayTransitions + public class TestTwoWayTransitions { private Recorder recorder; private StateMachine fsm; diff --git a/src/StateMachine/HybridStateMachine.cs b/src/StateMachine/HybridStateMachine.cs index 3a50074..6a38670 100644 --- a/src/StateMachine/HybridStateMachine.cs +++ b/src/StateMachine/HybridStateMachine.cs @@ -44,7 +44,9 @@ public HybridStateMachine( Action> afterOnExit = null, bool needsExitTime = false, - bool isGhostState = false) : base(needsExitTime, isGhostState) + bool isGhostState = false, + bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) { this.beforeOnEnter = beforeOnEnter; this.afterOnEnter = afterOnEnter; @@ -144,12 +146,14 @@ public HybridStateMachine( Action> afterOnExit = null, bool needsExitTime = false, - bool isGhostState = false) : base( + bool isGhostState = false, + bool rememberLastState = false) : base( beforeOnEnter, afterOnEnter, beforeOnLogic, afterOnLogic, beforeOnExit, afterOnExit, needsExitTime, - isGhostState + isGhostState, + rememberLastState ) { } @@ -170,12 +174,14 @@ public HybridStateMachine( Action> afterOnExit = null, bool needsExitTime = false, - bool isGhostState = false) : base( + bool isGhostState = false, + bool rememberLastState = false) : base( beforeOnEnter, afterOnEnter, beforeOnLogic, afterOnLogic, beforeOnExit, afterOnExit, needsExitTime, - isGhostState + isGhostState, + rememberLastState ) { } @@ -196,12 +202,14 @@ public HybridStateMachine( Action> afterOnExit = null, bool needsExitTime = false, - bool isGhostState = false) : base( + bool isGhostState = false, + bool rememberLastState = false) : base( beforeOnEnter, afterOnEnter, beforeOnLogic, afterOnLogic, beforeOnExit, afterOnExit, needsExitTime, - isGhostState + isGhostState, + rememberLastState ) { } diff --git a/src/StateMachine/StateMachine.cs b/src/StateMachine/StateMachine.cs index 42faf91..911eff3 100644 --- a/src/StateMachine/StateMachine.cs +++ b/src/StateMachine/StateMachine.cs @@ -92,8 +92,18 @@ private static readonly List> noTransitions private static readonly Dictionary>> noTriggerTransitions = new Dictionary>>(0); + /// + /// Event that is raised when the active state changes. + /// + /// + /// It is triggered when the state machine enters its initial state, and after a transition is performed. + /// Note that it is not called when the state machine exits. + /// + public event Action> StateChanged; + private (TStateId state, bool hasState) startState = (default, false); private PendingTransition pendingTransition = default; + private bool rememberLastState = false; // Central storage of states. private Dictionary stateBundlesByName @@ -108,8 +118,6 @@ private List> transitionsFromAny private Dictionary>> triggerTransitionsFromAny = new Dictionary>>(); - public event Action> OnActiveStateChanged; - public StateBase ActiveState { get @@ -132,11 +140,14 @@ public StateBase ActiveState /// (Only for hierarchical states): /// Determines whether the state machine as a state of a parent state machine is allowed to instantly /// exit on a transition (false), or if it should wait until an explicit exit transition occurs. + /// (Only for hierarchical states): + /// If true, the state machine will return to its last active state when it enters, instead + /// of to its original start state. /// - public StateMachine(bool needsExitTime = false, bool isGhostState = false) + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) : base(needsExitTime: needsExitTime, isGhostState: isGhostState) { - + this.rememberLastState = rememberLastState; } /// @@ -201,17 +212,16 @@ private void ChangeState(TStateId name, ITransitionListener listener = null) activeTriggerTransitions = bundle.triggerToTransitions ?? noTriggerTransitions; activeState = bundle.state; - OnActiveStateChanged?.Invoke(activeState); activeState.OnEnter(); - for (int i = 0; i < activeTransitions.Count; i++) + for (int i = 0, count = activeTransitions.Count; i < count; i++) { activeTransitions[i].OnEnter(); } foreach (List> transitions in activeTriggerTransitions.Values) { - for (int i = 0; i < transitions.Count; i++) + for (int i = 0, count = transitions.Count; i < count; i++) { transitions[i].OnEnter(); } @@ -219,6 +229,8 @@ private void ChangeState(TStateId name, ITransitionListener listener = null) listener?.AfterTransition(); + StateChanged?.Invoke(activeState); + if (activeState.isGhostState) { TryAllDirectTransitions(); @@ -315,7 +327,7 @@ private bool TryTransition(TransitionBase transition) /// Returns true if a transition occurred. private bool TryAllGlobalTransitions() { - for (int i = 0; i < transitionsFromAny.Count; i++) + for (int i = 0, count = transitionsFromAny.Count; i < count; i++) { TransitionBase transition = transitionsFromAny[i]; @@ -336,7 +348,7 @@ private bool TryAllGlobalTransitions() /// Returns true if a transition occurred. private bool TryAllDirectTransitions() { - for (int i = 0; i < activeTransitions.Count; i++) + for (int i = 0, count = activeTransitions.Count; i < count; i++) { TransitionBase transition = activeTransitions[i]; @@ -373,14 +385,14 @@ public override void OnEnter() ChangeState(startState.state); - for (int i = 0; i < transitionsFromAny.Count; i++) + for (int i = 0, count = transitionsFromAny.Count; i < count; i++) { transitionsFromAny[i].OnEnter(); } foreach (List> transitions in triggerTransitionsFromAny.Values) { - for (int i = 0; i < transitions.Count; i++) + for (int i = 0, count = transitions.Count; i < count; i++) { transitions[i].OnEnter(); } @@ -408,14 +420,18 @@ public override void OnLogic() public override void OnExit() { - if (activeState != null) + if (activeState == null) + return; + + if (rememberLastState) { - activeState.OnExit(); - // By setting the activeState to null, the state's onExit method won't be called - // a second time when the state machine enters again (and changes to the start state). - activeState = null; - OnActiveStateChanged?.Invoke(activeState); + startState = (activeState.name, true); } + + activeState.OnExit(); + // By setting the activeState to null, the state's onExit method won't be called + // a second time when the state machine enters again (and changes to the start state). + activeState = null; } public override void OnExitRequest() @@ -655,7 +671,7 @@ private bool TryTrigger(TEvent trigger) if (triggerTransitionsFromAny.TryGetValue(trigger, out triggerTransitions)) { - for (int i = 0; i < triggerTransitions.Count; i++) + for (int i = 0, count = triggerTransitions.Count; i < count; i++) { TransitionBase transition = triggerTransitions[i]; @@ -669,7 +685,7 @@ private bool TryTrigger(TEvent trigger) if (activeTriggerTransitions.TryGetValue(trigger, out triggerTransitions)) { - for (int i = 0; i < triggerTransitions.Count; i++) + for (int i = 0, count = triggerTransitions.Count; i < count; i++) { TransitionBase transition = triggerTransitions[i]; @@ -773,24 +789,24 @@ public override string GetActiveHierarchyPath() public class StateMachine : StateMachine { - public StateMachine(bool needsExitTime = false, bool isGhostState = false) - : base(needsExitTime: needsExitTime, isGhostState: isGhostState) + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) { } } public class StateMachine : StateMachine { - public StateMachine(bool needsExitTime = false, bool isGhostState = false) - : base(needsExitTime: needsExitTime, isGhostState: isGhostState) + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) { } } public class StateMachine : StateMachine { - public StateMachine(bool needsExitTime = false, bool isGhostState = false) - : base(needsExitTime: needsExitTime, isGhostState: isGhostState) + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) { } } diff --git a/src/States/ActionState.cs b/src/States/ActionState.cs index 1007490..b218a5e 100644 --- a/src/States/ActionState.cs +++ b/src/States/ActionState.cs @@ -12,6 +12,10 @@ public class ActionState : StateBase, IActionable actionStorage; + /// + /// Initialises a new instance of the ActionState class. + /// + /// public ActionState(bool needsExitTime, bool isGhostState = false) : base(needsExitTime: needsExitTime, isGhostState: isGhostState) { @@ -69,6 +73,7 @@ public void OnAction(TEvent trigger, TData data) /// public class ActionState : ActionState { + /// public ActionState(bool needsExitTime, bool isGhostState = false) : base(needsExitTime: needsExitTime, isGhostState: isGhostState) { @@ -78,6 +83,7 @@ public ActionState(bool needsExitTime, bool isGhostState = false) /// public class ActionState : ActionState { + /// public ActionState(bool needsExitTime, bool isGhostState = false) : base(needsExitTime: needsExitTime, isGhostState: isGhostState) { diff --git a/src/States/ParallelStates.cs b/src/States/ParallelStates.cs new file mode 100644 index 0000000..e069347 --- /dev/null +++ b/src/States/ParallelStates.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; + +namespace UnityHFSM +{ + /// + /// A state that can run multiple states in parallel. + /// + /// + /// If needsExitTime is set to true, it will exit when *any* one of the child states calls StateCanExit() + /// on this class. Note that having multiple child states that all do not need exit time and hence don't + /// call the StateCanExit() method, will mean that this state will never exit. + /// This behaviour can be overridden by specifying a canExit function that determines when this state may exit. + /// This will ignore the needsExitTime and StateCanExit() calls of the child states. It works the same as the + /// canExit feature of the State class. + /// + public class ParallelStates : StateBase, IActionable, IStateMachine + { + private List> states = new List>(); + + // When the states are passed in via the constructor, they are not assigned names / identifiers. + // This means that the active hierarchy path cannot include them (which would be used for debugging purposes). + private bool areStatesNameless = false; + + // This variable keeps track whether this state is currently active. It is used to prevent + // StateCanExit() calls from the child states to be passed on to the parent state machine + // when this state is no longer active, which would result in unwanted behaviour + // (e.g. two transitions). + private bool isActive; + + private Func, bool> canExit; + + public bool HasPendingTransition => fsm.HasPendingTransition; + public IStateMachine ParentFsm => fsm; + + /// + public ParallelStates( + Func, bool> canExit = null, + bool needsExitTime = false, + bool isGhostState = false) : base(needsExitTime, isGhostState) + { + this.canExit = canExit; + } + + /// + public ParallelStates(params StateBase[] states) + : this(null, false, false, states) { } + + /// + public ParallelStates(bool needsExitTime, params StateBase[] states) + : this(null, needsExitTime, false, states) { } + + /// + public ParallelStates( + Func, bool> canExit, + bool needsExitTime, + params StateBase[] states) : this(canExit, needsExitTime, false, states) { } + + /// + /// Initialises a new instance of the ParallelStates class. + /// + /// (Only if needsExitTime is true): + /// Function that determines if the state is ready to exit (true) or not (false). + /// It is called OnExitRequest and on each logic step when a transition is pending. + /// States to run in parallel. Note that they are not assigned names / identifiers + /// and will therefore not be included in the active hierarchy path. If this is unwanted, + /// add the states using AddState() instead. + /// + public ParallelStates( + Func, bool> canExit, + bool needsExitTime, + bool isGhostState, + params StateBase[] states) : base(needsExitTime, isGhostState) + { + this.canExit = canExit; + this.areStatesNameless = true; + + foreach (var state in states) + { + AddState(default, state); + } + } + + /// + /// Adds a new state that is run in parallel while this state is active. + /// + /// Name / identifier of the state. This is only used for debugging purposes. + /// State to add. + /// Itself to allow for a fluent interface. + public ParallelStates AddState(TStateId id, StateBase state) + { + state.fsm = this; + state.name = id; + state.Init(); + + states.Add(state); + + // Fluent interface. + return this; + } + + public override void Init() + { + foreach (var state in states) + { + state.fsm = this; + } + } + + public override void OnEnter() + { + isActive = true; + + foreach (var state in states) + { + state.OnEnter(); + } + } + + public override void OnLogic() + { + foreach (var state in states) + { + state.OnLogic(); + } + + if (needsExitTime && canExit != null && fsm.HasPendingTransition && canExit(this)) + { + fsm.StateCanExit(); + } + } + + public override void OnExit() + { + isActive = false; + + foreach (var state in states) + { + state.OnExit(); + } + } + + public override void OnExitRequest() + { + // When this state machine is requested to exit, check each child state to see if any one is + // ready to exit. This behaviour can be overridden by providing a canExit function. + if (canExit == null) + { + foreach (var state in states) + { + state.OnExitRequest(); + } + } + else + { + if (fsm.HasPendingTransition && canExit(this)) + { + fsm.StateCanExit(); + } + } + } + + public void OnAction(TEvent trigger) + { + foreach (var state in states) + { + (state as IActionable)?.OnAction(trigger); + } + } + + public void OnAction(TEvent trigger, TData data) + { + foreach (var state in states) + { + (state as IActionable)?.OnAction(trigger, data); + } + } + + public void StateCanExit() + { + // Try to exit as soon as any one of the child states can exit, unless the exit behaviour + // is overridden by canExit. + if (isActive && canExit == null) + { + fsm.StateCanExit(); + } + } + + public override string GetActiveHierarchyPath() + { + // The name could be null when ParallelStates is used at the top level. + string stringName = this.name?.ToString() ?? ""; + + if (areStatesNameless || states.Count == 0) + { + // Example path: "Parallel" + return stringName; + } + + if (states.Count == 1) + { + // Example path: "Parallel/Move" + return stringName + "/" + states[0].GetActiveHierarchyPath(); + } + + // Example path: "Parallel/(Move & Attack/Shoot)" + string path = stringName + "/("; + + for (int i = 0; i < states.Count; i++) + { + path += states[i].GetActiveHierarchyPath(); + if (i < states.Count - 1) + { + path += " & "; + } + } + + return path + ")"; + } + } + + /// + public class ParallelStates : ParallelStates + { + /// + public ParallelStates( + Func, bool> canExit = null, + bool needsExitTime = false, + bool isGhostState = false) : base(canExit, needsExitTime, isGhostState) { } + + /// + public ParallelStates(params StateBase[] states) + : base(null, false, false, states) { } + + /// + public ParallelStates(bool needsExitTime, params StateBase[] states) + : base(null, needsExitTime, false, states) { } + + /// + public ParallelStates( + Func, bool> canExit, + bool needsExitTime, + params StateBase[] states) : base(canExit, needsExitTime, false, states) { } + + /// + public ParallelStates( + Func, bool> canExit, + bool needsExitTime, + bool isGhostState, + params StateBase[] states) : base(canExit, needsExitTime, isGhostState, states) { } + } + + public class ParallelStates : ParallelStates + { + /// + public ParallelStates( + Func, bool> canExit = null, + bool needsExitTime = false, + bool isGhostState = false) : base(canExit, needsExitTime, isGhostState) { } + + /// + public ParallelStates(params StateBase[] states) + : base(null, false, false, states) { } + + /// + public ParallelStates(bool needsExitTime, params StateBase[] states) + : base(null, needsExitTime, false, states) { } + + /// + public ParallelStates( + Func, bool> canExit, + bool needsExitTime, + params StateBase[] states) : base(canExit, needsExitTime, false, states) { } + + /// + public ParallelStates( + Func, bool> canExit, + bool needsExitTime, + bool isGhostState, + params StateBase[] states) : base(canExit, needsExitTime, isGhostState, states) { } + } + + public class ParallelStates : ParallelStates + { + /// + public ParallelStates( + Func, bool> canExit = null, + bool needsExitTime = false, + bool isGhostState = false) : base(canExit, needsExitTime, isGhostState) { } + + /// + public ParallelStates(params StateBase[] states) + : this(null, false, false, states) { } + + /// + public ParallelStates(bool needsExitTime, params StateBase[] states) + : this(null, needsExitTime, false, states) { } + + /// + public ParallelStates( + Func, bool> canExit, + bool needsExitTime, + params StateBase[] states) : this(canExit, needsExitTime, false, states) { } + + /// States to run in parallel. They are implicitly assigned names + /// based on their indices (e.g. the first state has the name "0", ...) which is + /// useful for debugging. + /// + public ParallelStates( + Func, bool> canExit, + bool needsExitTime, + bool isGhostState, + params StateBase[] states) : base(canExit, needsExitTime, isGhostState) + { + for (int i = 0; i < states.Length; i++) + { + AddState(i.ToString(), states[i]); + } + } + } +} \ No newline at end of file diff --git a/src/States/ParallelStates.cs.meta b/src/States/ParallelStates.cs.meta new file mode 100644 index 0000000..59f6798 --- /dev/null +++ b/src/States/ParallelStates.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b3eff52e7c122a43a00268a9ee341ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/States/State.cs b/src/States/State.cs index 182c830..d2305bb 100644 --- a/src/States/State.cs +++ b/src/States/State.cs @@ -53,12 +53,15 @@ public override void OnEnter() public override void OnLogic() { + onLogic?.Invoke(this); + + // Check whether the state is ready to exit after calling onLogic, as it may trigger a transition. + // Calling onLogic beforehand would lead to invalid behaviour as it would be called, even though this state + // is not active anymore. if (needsExitTime && canExit != null && fsm.HasPendingTransition && canExit(this)) { fsm.StateCanExit(); } - - onLogic?.Invoke(this); } public override void OnExit() diff --git a/src/States/StateWrapper.cs b/src/States/StateWrapper.cs index d31c3ee..fe2282d 100644 --- a/src/States/StateWrapper.cs +++ b/src/States/StateWrapper.cs @@ -95,6 +95,11 @@ public void OnAction(TEvent trigger, TData data) { (state as IActionable)?.OnAction(trigger, data); } + + public override string GetActiveHierarchyPath() + { + return state.GetActiveHierarchyPath(); + } } private Action> diff --git a/src/Transitions/TransitionOnKey.cs b/src/Transitions/TransitionOnKey.cs index 419791e..7bb5f8c 100644 --- a/src/Transitions/TransitionOnKey.cs +++ b/src/Transitions/TransitionOnKey.cs @@ -28,7 +28,7 @@ public override bool ShouldTransition() } } - public class Release : TransitionBase + public class Released : TransitionBase { private KeyCode keyCode; @@ -37,7 +37,7 @@ public class Release : TransitionBase /// It behaves like Input.GetKeyUp(...). /// /// The KeyCode of the key to watch. - public Release( + public Released( TStateId from, TStateId to, KeyCode key, @@ -52,7 +52,7 @@ public override bool ShouldTransition() } } - public class Press : TransitionBase + public class Pressed : TransitionBase { private KeyCode keyCode; @@ -61,7 +61,7 @@ public class Press : TransitionBase /// It behaves like Input.GetKeyDown(...). /// /// The KeyCode of the key to watch. - public Press( + public Pressed( TStateId from, TStateId to, KeyCode key, @@ -82,7 +82,7 @@ public class Up : TransitionBase /// /// Initialises a new transition that triggers, while a key is up. - /// It behaves like ! Input.GetKey(...). + /// It behaves like !Input.GetKey(...). /// /// The KeyCode of the key to watch. public Up( @@ -111,9 +111,9 @@ public Down( } } - public class Release : Release + public class Released : Released { - public Release( + public Released( string @from, string to, KeyCode key, @@ -122,9 +122,9 @@ public Release( } } - public class Press : Press + public class Pressed : Pressed { - public Press( + public Pressed( string @from, string to, KeyCode key, diff --git a/src/Transitions/TransitionOnMouse.cs b/src/Transitions/TransitionOnMouse.cs index d1f9269..fc9aec9 100644 --- a/src/Transitions/TransitionOnMouse.cs +++ b/src/Transitions/TransitionOnMouse.cs @@ -29,7 +29,7 @@ public override bool ShouldTransition() } } - public class Release : TransitionBase + public class Released : TransitionBase { private int button; @@ -38,7 +38,7 @@ public class Release : TransitionBase /// It behaves like Input.GetMouseButtonUp(...). /// /// The mouse button to watch. - public Release( + public Released( TStateId from, TStateId to, int button, @@ -53,7 +53,7 @@ public override bool ShouldTransition() } } - public class Press : TransitionBase + public class Pressed : TransitionBase { private int button; @@ -62,7 +62,7 @@ public class Press : TransitionBase /// It behaves like Input.GetMouseButtonDown(...). /// /// The mouse button to watch. - public Press( + public Pressed( TStateId from, TStateId to, int button, @@ -83,7 +83,7 @@ public class Up : TransitionBase /// /// Initialises a new transition that triggers, while a mouse button is up. - /// It behaves like ! Input.GetMouseButton(...). + /// It behaves like !Input.GetMouseButton(...). /// /// The mouse button to watch. public Up( @@ -112,9 +112,9 @@ public Down( } } - public class Release : Release + public class Released : Released { - public Release( + public Released( string @from, string to, int button, @@ -123,9 +123,9 @@ public Release( } } - public class Press : Press + public class Pressed : Pressed { - public Press( + public Pressed( string @from, string to, int button, diff --git a/src/Util/Timer.cs b/src/Util/Timer.cs index 8b8f2c4..b9306ff 100644 --- a/src/Util/Timer.cs +++ b/src/Util/Timer.cs @@ -10,11 +10,6 @@ public class Timer : ITimer public float startTime; public float Elapsed => Time.time - startTime; - public Timer() - { - startTime = Time.time; - } - public void Reset() { startTime = Time.time;