diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..035816b --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,20 @@ +name: Release + +on: + release: + types: [created, published] + +jobs: + package: + name: Package src to release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Package source + run: cd src && zip -r RobloxStateMachine.zip StateMachine/ + + - name: Upload source to release + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload ${{ github.ref_name }} src/RobloxStateMachine.zip diff --git a/README.md b/README.md index 051768b..602b2b9 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ robloxstatemachine = "prooheckcp/robloxstatemachine@>0.0.0, <10.0.0" You can start learning in the docs page! https://prooheckcp.github.io/RobloxStateMachine/docs/intro ## šŸ“·Tutorial -[RobloxStateMachine Tutorial (Studio) - YouTube](https://www.youtube.com/watch?v=7M1LkjPaEFE&ab_channel=Prooheckcp) +[![[RobloxStateMachine Tutorial (Studio) - YouTube]](gh-assets/tut-embed.png)](https://www.youtube.com/watch?v=7M1LkjPaEFE&ab_channel=Prooheckcp) # ⭐ Contributing Please leave a star on [GitHub](https://github.com/prooheckcp/RobloxStateMachine), it helps a lot! @@ -39,4 +39,4 @@ to discuss what you would like to change. Please make sure to update tests as appropriate. # šŸ“„ License -[MIT](https://choosealicense.com/licenses/mit/) \ No newline at end of file +[MIT](https://choosealicense.com/licenses/mit/) diff --git a/gh-assets/tut-embed.png b/gh-assets/tut-embed.png new file mode 100644 index 0000000..b517587 Binary files /dev/null and b/gh-assets/tut-embed.png differ diff --git a/src/StateMachine/Classes/State.lua b/src/StateMachine/Classes/State.lua index b78b468..8e20388 100644 --- a/src/StateMachine/Classes/State.lua +++ b/src/StateMachine/Classes/State.lua @@ -5,7 +5,7 @@ local mergeTables = require(script.Parent.Parent.Functions.mergeTables) @class State Describes one of the many states an object can have. It also declares - how it should behave when it enters, is and leaves the given state + how it should behave when it enters, runs and leaves the given state ]=] local State = {} State.__index = State @@ -14,7 +14,7 @@ State.Type = "State" @prop Name string @within State - The name of the state. This is used to identify the state. Usually set while creating the state + The name of the state. This is used to identify the state. (Usually set while creating the state) ```lua local Blue: State = State.new("Blue") @@ -25,7 +25,7 @@ State.Name = "" :: string @prop Transitions string @within State - A reference for the transitions of this state. This is usually set while creating the state + A reference for the transitions of this state. This is (usually set while creating the state) ```lua local GoToBlue = require(script.Parent.Parent.Transitions.GoToBlue) @@ -41,7 +41,7 @@ State.Transitions = {} :: {Transition.Transition} @prop Data {[string]: any} @within State - Contains the state machine data, it can be accessed from within the class + Contains the custom state machine data. It can be accessed from within the class ]=] State.Data = {} :: {[string]: any} --[=[ @@ -57,7 +57,7 @@ State._transitions = {} :: {[string]: Transition.Transition} @within State @private - This is used to change the state of the state machine. This is set by the state machine itself + This is used to change the state of the state machine. (This is set by the state machine itself) ]=] State._changeState = nil :: (newState: string)->()? --[=[ @@ -65,7 +65,7 @@ State._changeState = nil :: (newState: string)->()? @within State @private - This is used to change the data of the state machine. This is set by the state machine itself + This is used to change the custom data of the state machine. (This is set by the state machine itself) ]=] State._changeData = nil :: (index: string, newValue: any)-> ()? --[=[ @@ -73,7 +73,7 @@ State._changeData = nil :: (index: string, newValue: any)-> ()? @within State @private - Gets the current state of our state machine + Gets the current state of our state machine. (This is set by the state machine itself) ]=] State._getState = nil :: (index: string, newValue: any)-> string --[=[ @@ -81,13 +81,14 @@ State._getState = nil :: (index: string, newValue: any)-> string @within State @private - Gets the previous state of our state machine + Gets the previous state of our state machine. (This is set by the state machine itself) ]=] State._getPreviousState = nil :: ()-> string? --[=[ Used to create a new State. The state should manage how the object should behave during - that given state. I personally recommend having your states in their own files for organizational - purposes + that given state. + + (I personally recommend having your states in their own files for organizational purposes) ```lua local ReplicatedStorage = game:GetService("ReplicatedStorage") @@ -121,7 +122,7 @@ function State.new(stateName: string?): State end --[=[ - Extends a state inheriting the behavior from the parent state + Extends a state, inheriting the behavior from the parent state ```lua local state = State.new("Blue") @@ -146,7 +147,9 @@ function State:Extend(stateName: string): State end --[=[ - Forcelly changes the current state of our state machine to a new one + Changes the current state of our state machine to a new one. + + _(**currentState:CanChangeState** must be satisfied before it can change!)_ @param newState string -- The name of the new state @@ -157,6 +160,7 @@ function State:ChangeState(newState: string): () return end + -- Call the "parent" StateMachine Method self._changeState(newState) end @@ -170,6 +174,7 @@ function State:GetState(): string return "" end + -- Call the "parent" StateMachine Method return self._getState() end @@ -183,11 +188,14 @@ function State:GetPreviousState(): string return "" end + -- Call the "parent" StateMachine Method return self._getPreviousState() end --[=[ - Changing data request. You can also just Get the data and change the data at run time. + Changing the custom data, while firing **DataChanged** Event + + (You can also just use the date argument and change the data at runtime, _**However** this dose not fire **DataChanged** event!_) ```lua local example: State = State.new("Blue") @@ -209,6 +217,7 @@ function State:ChangeData(index: string, newValue: any): () return end + -- Call the "parent" StateMachine Method self._changeData(index, newValue) end @@ -217,7 +226,7 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: - Called whenever a state machine is created with this state. + Called whenever a state machine is newly created with this state ```lua function State:OnInit(data) @@ -226,7 +235,7 @@ end end ``` - @param _data {[string]: any} -- This is the data from the StateMachine itself! + @param _data {[string]: any} -- This is the custom data from the parent StateMachine @return () ]=] @@ -239,6 +248,10 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: + _(Forcefully)_ Determines if the state machine is allowed to switch states or not. + + _(It's a good way to lock the current state)_ + ```lua function State:CanChangeState(targetState: string) return targetState == "Blue" -- If the target state is blue, we can change to it @@ -262,7 +275,7 @@ end **OnDataChanged** only gets called when the data is changed by a **ChangeData** call ::: - Called whenever the data of the state machine changes. + Called whenever the data of the state machine changes ```lua function State:OnDataChanged(data, index, newValue, oldValue) @@ -272,7 +285,7 @@ end end ``` - @param _data {[string]: any} -- This is the data from the StateMachine itself! + @param _data {[string]: any} -- This is the custom data from the parent StateMachine @param _index any -- The index of the data that changed @param _value any -- The new value of the data @param _oldValue any -- The old value of the data @@ -288,7 +301,13 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: - Called whenever you enter this state + Called whenever you first enter this state. + + :::warning + **OnHeartbeat** dose not wait for OnEnter to finish. You must implement + that yourself. + ::: + ```lua function State:OnEnter(data) @@ -296,7 +315,7 @@ end end ``` - @param _data {[string]: any} -- This is the data from the StateMachine itself! + @param _data {[string]: any} -- This is the custom data from the parent StateMachine @return () ]=] @@ -309,7 +328,9 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: - Called every frame after the physics simulation while in this state + Called every frame (after the physics simulation) while in this state. + + (See [`RunService.Heartbeat`](https://create.roblox.com/docs/reference/engine/classes/RunService#Heartbeat) for more information) ```lua function Default:OnHeartbeat(data, deltaTime: number) @@ -317,7 +338,7 @@ end end ``` - @param _data {[string]: any} -- This is the data from the StateMachine itself! + @param _data {[string]: any} -- This is the custom data from the parent StateMachine @param _deltaTime number @return () @@ -331,7 +352,7 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: - Called whenever you leave this state + Called whenever your about leave this state. _(and before **OnDestroy**)_ ```lua function State:OnLeave(data) @@ -339,7 +360,7 @@ end end ``` - @param _data {[string]: any} -- This is the data from the StateMachine itself! + @param _data {[string]: any} -- This is the custom data from the parent StateMachine @return () ]=] @@ -352,7 +373,7 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: - Called whenever the state machine is destroyed + Called whenever the state machine is being destroyed ```lua function State:OnDestroy() diff --git a/src/StateMachine/Classes/Transition.lua b/src/StateMachine/Classes/Transition.lua index 063633e..d202193 100644 --- a/src/StateMachine/Classes/Transition.lua +++ b/src/StateMachine/Classes/Transition.lua @@ -12,7 +12,7 @@ Transition.Type = "Transition" @prop Name string @within Transition - The name of the state. This is used to identify the state. Usually set while creating the state + The name of the transition. This is used to identify this transition. _(Not required)_ ```lua local Transition = StateMachine.Transition @@ -26,7 +26,7 @@ Transition.Name = "" :: string @prop TargetState string @within Transition - The name of the state. This is used to identify the state. Usually set while creating the state + The name of the state to change to when transitioning. (Usually set while creating the transition) ```lua local Transition = StateMachine.Transition @@ -40,10 +40,10 @@ Transition.TargetState = "" :: string @prop Data {[string]: any} @within Transition - Contains the state machine data, it can be accessed from within the class + Contains the custom state machine data. It can be accessed from within the class ```lua - local Default: State = State.new("Blue") + local Default: State = Transition.new("Blue") function Default:OnInit(data) print(self.Data) @@ -56,7 +56,7 @@ Transition.Data = {} :: {[string]: any} @within Transition @private - This is used to change the state of the state machine. This is set by the state machine itself + Gets the current state of our state machine. (This is set by the state machine itself) ]=] Transition._changeState = nil :: (newState: string)->()? --[=[ @@ -64,7 +64,7 @@ Transition._changeState = nil :: (newState: string)->()? @within Transition @private - This is used to change the data of the state machine. This is set by the state machine itself + This is used to change the custom data of the state machine. (This is set by the state machine itself) ]=] Transition._changeData = nil :: (index: string, newValue: any)-> ()? --[=[ @@ -72,7 +72,7 @@ Transition._changeData = nil :: (index: string, newValue: any)-> ()? @within Transition @private - Gets the current state of our state machine + Gets the current state of our state machine. (This is set by the state machine itself) ]=] Transition._getState = nil :: (index: string, newValue: any)-> string --[=[ @@ -80,15 +80,16 @@ Transition._getState = nil :: (index: string, newValue: any)-> string @within Transition @private - Gets the previous state of our state machine + Gets the previous state of our state machine. (This is set by the state machine itself) ]=] Transition._getPreviousState = nil :: ()-> string? --[=[ - Creates a new transition. Transitions are used to tell our state - when and how should it move from the current state to a different one. - They are meant to be reusable and allow us to easily add and reuse transitions - between states and objects + Creates a new transition. + + Transitions are used to tell our state when and how should it move from the + current state to a different one. They are meant to be reusable and allow us + to easily add and reuse transitions between states and objects. ```lua local ReplicatedStorage = game:GetService("ReplicatedStorage") @@ -97,7 +98,7 @@ Transition._getPreviousState = nil :: ()-> string? local Transition = StateMachine.Transition local GoToBlue = Transition.new("Blue") - GoToBlue.OnHearbeat = false + GoToBlue.OnHeartbeat = false function GoToBlue:OnDataChanged(data) return tick() - data.time > 10 -- Will change to blue after 10 seconds @@ -118,7 +119,7 @@ function Transition.new(targetState: string?): Transition end --[=[ - Extends a state inheriting the behavior from the parent state + Extends a state, inheriting the behavior from the parent state ```lua local transition = Transition.new("Blue") @@ -147,7 +148,7 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: - Called whenever the state machine is created + Called whenever a state machine is newly created with this transition ```lua function Transition:OnInit() @@ -155,7 +156,7 @@ end end ``` - @param _data {[string]: any} -- This is the data from the StateMachine itself! + @param _data {[string]: any} -- This is the custom data from the parent StateMachine @return () ]=] @@ -168,7 +169,12 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: - Called whenever you enter this transition object + Called whenever you first enter this transition object + + :::warning + **OnDataChanged** dose not wait for OnEnter to finish. You must implement + that yourself. + ::: ```lua function State:OnEnter(data) @@ -176,7 +182,7 @@ end end ``` - @param _data {[string]: any} -- This is the data from the StateMachine itself! + @param _data {[string]: any} -- This is the custom data from the parent StateMachine @return () ]=] @@ -189,7 +195,7 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: - Called whenever you leave this transition object + Called whenever your about leave this transition. _(and before **OnDestroy**)_ ```lua function State:OnLeave(data) @@ -197,7 +203,7 @@ end end ``` - @param _data {[string]: any} -- This is the data from the StateMachine itself! + @param _data {[string]: any} -- This is the custom data from the parent StateMachine @return () ]=] @@ -211,7 +217,7 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: - @deprecated v1.1.7 -- This function is redundant since it essencially just works as a blocker for OnDataChanged + @deprecated v1.1.7 -- This function is redundant since it essentially just works as a blocker for OnDataChanged Whether it can change to this state or not. It's a good way to lock the current state @@ -225,7 +231,7 @@ function Transition:CanChangeState(data: {[string]: any}): boolean end --[=[ - Forcelly changes the current state of our state machine to a new one + Changes the current state of our state machine to a new one. @param newState string -- The name of the new state @@ -236,6 +242,7 @@ function Transition:ChangeState(newState: string): () return end + -- Call the "parent" StateMachine Method self._changeState(newState) end @@ -244,6 +251,8 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: + Determines wether the transition should should change yet. + Should return true if it should change to the target state and false if it shouldn't @@ -266,6 +275,7 @@ function Transition:GetState(): string return "" end + -- Call the "parent" StateMachine Method return self._getState() end @@ -279,11 +289,14 @@ function Transition:GetPreviousState(): string return "" end + -- Call the "parent" StateMachine Method return self._getPreviousState() end --[=[ - Changing data request. You can also just Get the data and change the data at run time. + Changing the custom data, while firing **DataChanged** Event + + (You can also just use the date argument and change the data at runtime, _**However** this dose not fire **DataChanged** event!_) ```lua local example: State = State.new("Blue") @@ -305,6 +318,7 @@ function Transition:ChangeData(index: string, newValue: any): () return end + -- Call the "parent" StateMachine Method self._changeData(index, newValue) end @@ -313,7 +327,7 @@ end This is a **Virtual Method**. Virtual Methods are meant to be overwritten ::: - Called whenever the state machine is destroyed + Called whenever the state machine is being destroyed ```lua function Transition:OnDestroy() diff --git a/src/StateMachine/init.lua b/src/StateMachine/init.lua index 8e47c7b..8515a1d 100644 --- a/src/StateMachine/init.lua +++ b/src/StateMachine/init.lua @@ -30,7 +30,7 @@ StateMachine.__index = StateMachine @prop Data {[string]: any} @within StateMachine - Contains the data that is shared accross all states and transitions of this state machine. Should be accessed with :GetData + Contains the data that is shared across all states and transitions of this state machine. Should be accessed with :GetData E.g ```lua @@ -38,7 +38,7 @@ StateMachine.__index = StateMachine stateMachine:GetData().health = 50 ``` - The data is shared accross all states and transitions. It can be access in 2 different ways + The data is shared across all states and transitions. It can be access in 2 different ways ```lua --transition.lua @@ -62,7 +62,7 @@ StateMachine.Data = {} :: {[string]: any} @prop StateChanged⚔ Signal<(string, string)>? @within StateMachine - Called whenever the state of this state machinse changes. The first argument + Called whenever the state of this state machine changes. The first argument is the new state and the second one is the previous state. If there was no previous state then it will be an empty string @@ -172,8 +172,8 @@ StateMachine._Destroyed = false :: boolean ) ``` - @param initialState string -- The name of the state at which it should start - @param states {State.State} -- An array of the states this state machine should have + @param initialState string -- The name of the State at which it should start + @param states {State.State} -- An array of the states this State machine should have @param initialData {[string]: any}? -- The starting data to be used by the states @return RobloxStateMachine @@ -191,13 +191,17 @@ function StateMachine.new(initialState: string, states: {State}, initialData: {[ self.StateChanged = Signal.new() :: Signal.Signal<(string, string)> self.DataChanged = Signal.new() :: Signal.Signal<({[string]: any}, any, any, any)>? - for _, state: State.State in states do -- Load the states + -- Load all the states + for _, state: State.State in states do if self._States[state.Name] then error(DUPLICATE_ERROR.." \""..state.Name.."\"", 2) end + -- Create a copy of the State "parented" to this StateMachine local stateClone: State.State = Copy(state) stateClone.Data = self.Data + + -- Fill up the necessary "parent" accessing methods with our methods stateClone._changeState = function(newState: string) self:ChangeState(newState) end @@ -211,14 +215,22 @@ function StateMachine.new(initialState: string, states: {State}, initialData: {[ return self:GetPreviousState() end + -- Load all the Transitions stateClone._transitions = {} - for _, transition: Transition in stateClone.Transitions do - if #transition.Name == 0 then + if #transition.Name == 0 then -- (Transitions don't need names, but must have one for HashTable) transition.Name = HttpService:GenerateGUID(false) end + -- Create a copy of the Transition "parented" to this StateMachine local transitionClone: Transition = Copy(transition) + + if transitionClone.Type ~= Transition.Type then -- (must be a Transition) + error(WRONG_TRANSITION, 2) + end + + -- Fill up the necessary "parent" accessing methods with our methods + transitionClone.Data = stateClone.Data transitionClone._changeData = function(index: string, newValue: any) self:ChangeData(index, newValue) end @@ -228,62 +240,68 @@ function StateMachine.new(initialState: string, states: {State}, initialData: {[ transitionClone._getPreviousState = function() return self:GetPreviousState() end - - if transitionClone.Type ~= Transition.Type then - error(WRONG_TRANSITION, 2) - end - - transitionClone.Data = stateClone.Data transitionClone._changeState = function(newState: string) self:ChangeState(newState) end + -- Add the Transition to the list of initialized Transitions stateClone._transitions[transitionClone.Name] = transitionClone + + -- Run its own initialization method and add it to be cleaned up on destruction task.spawn(transitionClone.OnInit, transitionClone, self.Data) self._trove:Add(transitionClone, "OnDestroy") end + -- Add the State to the list of initialized States self._States[state.Name] = stateClone + + -- Run its own initialization method and add it to be cleaned up on destruction task.spawn(stateClone.OnInit, stateClone, self.Data) self._trove:Add(stateClone, "OnDestroy") end - if not self._States[initialState] then + if not self._States[initialState] then -- (Make sure the staring State is valid) error(STATE_NOT_FOUND:format("create a state machine", initialState), 2) end local previousState: State = nil - self._trove:Add(RunService.Heartbeat:Connect(function(deltaTime: number) - if self._Destroyed then + self._trove:Connect(RunService.Heartbeat, function(deltaTime: number) + if self._Destroyed then -- (Don't run if destroyed) return end self:_CheckTransitions() local state = self:_GetCurrentStateObject() + + -- Skip the first frame of a State change local firstFrame: boolean = state ~= previousState previousState = state if firstFrame then return end + -- Don't run if nothing was changed if not state or getmetatable(state).OnHeartbeat == state.OnHeartbeat then return end - task.spawn(state.OnHeartbeat, state, self:GetData(), deltaTime) - end)) + -- Run the heartbeat method for the state + self:_CallMethod(state, false, "OnHeartbeat", self:GetData(), deltaTime) + end) + -- Add the Events to be cleaned on destruction self._trove:Add(self.StateChanged) self._trove:Add(self.DataChanged) + -- Start on the starting state self:_ChangeState(initialState) return self end --[=[ - Returns the current state of the State Machine + Returns the current state of the State Machine (in string form) ```lua local exampleStateMachine = RobloxStateMachine.new("Default", {}, {}) @@ -297,7 +315,7 @@ function StateMachine:GetCurrentState(): string end --[=[ - Returns the previous state of the State Machine + Returns the previous state of the State Machine (in string form) ```lua local exampleStateMachine = RobloxStateMachine.new("Default", {...BlueStateHere}, {}) @@ -312,7 +330,9 @@ function StateMachine:GetPreviousState(): string end --[=[ - Changing data request. You can also just Get the data and change the data at run time. + Changing the custom data, while firing **DataChanged** Event + + (You can also just use **GetData** and change the data at runtime, _**However** this dose not fire **DataChanged** event!_) ```lua local stateMachine = RobloxStateMachine.new("state", states, {health = 0}) @@ -331,16 +351,19 @@ function StateMachine:ChangeData(index: string, newValue: any): () return end + -- Change the data local oldValue: any = self.Data[index] self.Data[index] = newValue - local state: State = self._States[self:GetCurrentState()] - task.spawn(state.OnDataChanged, state, self.Data, index, newValue, oldValue) + local state: State = self:_GetCurrentStateObject() + + -- Call DataChanged Events + self:_CallMethod(state, false, "OnDataChanged", self.Data, index, newValue, oldValue) self.DataChanged:Fire(self.Data, index, newValue, oldValue) end --[=[ - Gets the custom data of this state machine object. + Gets the custom data of this state machine ```lua local stateMachine = RobloxStateMachine.new("Start", {state1, state2}, {health = 20}) @@ -351,6 +374,7 @@ end @return {[string]: any} ]=] function StateMachine:GetData(): {[string]: any} + -- Clear the data if it is not a table if typeof(self.Data) ~= "table" then warn(DATA_WARNING) self.Data = {} @@ -360,7 +384,9 @@ function StateMachine:GetData(): {[string]: any} end --[=[ - Used to load thru a directory. It's specially useful to load states and or transitions! + Used to load thru an entire directory (and its sub-directories). + + _**(Especially useful to load states and or transitions!)**_ ```lua local exampleStateMachine: RobloxStateMachine.RobloxStateMachine = RobloxStateMachine.new( @@ -373,7 +399,7 @@ end ) ``` - You can also use it to load specific files by feeding the names you wish to load + (You can also use it to load specific files by feeding the names you wish to load) @param directory Instance @@ -382,6 +408,7 @@ end @return {any} ]=] function StateMachine:LoadDirectory(directory: Instance, names: {string}?): {any} + -- Load from scratch if not already loaded in the past if not cacheDirectories[directory] then cacheDirectories[directory] = {} @@ -390,10 +417,12 @@ function StateMachine:LoadDirectory(directory: Instance, names: {string}?): {any continue end + -- Load the ModuleScript local success: boolean, result: any = pcall(function() return require(child) end) + -- Make sure it's actually a table if not success or typeof(result) ~= "table" @@ -401,24 +430,28 @@ function StateMachine:LoadDirectory(directory: Instance, names: {string}?): {any continue end + -- Make sure its a valid State or Transition if result.Type ~= State.Type and result.Type ~= Transition.Type then continue end + -- Use the name of the Script if no name found if not result.Name or result.Name == "" then result.Name = child.Name end + -- Save the result to be loaded quickly in the future table.insert(cacheDirectories[directory], result) end end + -- If there is nothing left to do, then return the saved Modules if not names then return cacheDirectories[directory] end + -- Only return the modules with the same name as in `names` local filteredFiles = {} - for _, file in cacheDirectories[directory] do if table.find(names, file.Name) then table.insert(filteredFiles, file) @@ -429,8 +462,9 @@ function StateMachine:LoadDirectory(directory: Instance, names: {string}?): {any end --[=[ - If you wish to stop using the state machine at any point you should then clear - it from the memory. Call Destroy whenever you are done with the state machine. + Clears all the memory used by the state machine + + (Use if you wish to stop using the state machine at any point) ```lua local stateMachine = RobloxStateMachine.new(...) @@ -449,26 +483,29 @@ function StateMachine:Destroy(): () self._Destroyed = true + -- Run the Leave method on the State before destroying local state: State? = self:_GetCurrentStateObject() - if state then task.spawn(state.OnLeave, state, self:GetData()) end + -- Clean up everything to save memory self._trove:Destroy() self._stateTrove:Destroy() end --[=[ - Forcelly changes the current state of our state machine to a new one + Changes the current state of our state machine to a new one. + + _(**currentState:CanChangeState** must be satisfied before it can change!)_ @param newState string -- The name of the new state @return () ]=] function StateMachine:ChangeState(newState: string): () + --Make sure we are allowed to change states local currentState: State? = self:_GetCurrentStateObject() - if currentState and not currentState:CanChangeState(newState) then return end @@ -490,7 +527,7 @@ function StateMachine:_StateExists(stateName: string): boolean end --[=[ - Called to change the current state of the state machine + Called to _truly_ change the current state of the state machine @private @@ -503,12 +540,15 @@ function StateMachine:_ChangeState(newState: string): () return end + -- Make sure the State even exists to begin with assert(self:_StateExists(newState), STATE_NOT_FOUND:format(`change to {newState}`, newState)) + -- Only swap if it's not the same if self._CurrentState == newState then return end + -- Get the updated State classes local previousState: State? = self:_GetCurrentStateObject() local state: State? = self._States[newState] @@ -516,19 +556,23 @@ function StateMachine:_ChangeState(newState: string): () return end - self._stateTrove:Clean() + -- Clean up the previous state if previousState then task.spawn(previousState.OnLeave, previousState, self:GetData()) self:_CallTransitions(previousState, "OnLeave", self:GetData()) end + self._stateTrove:Clean() + -- Switch to the new state task.defer(function() self:_CallTransitions(state, "OnEnter", self:GetData()) end) - self._stateTrove:Add(task.defer(state.OnEnter, state, self:GetData())) + self:_CallMethod(state, true, "OnEnter", self:GetData()) + -- Update the current state self._CurrentState = newState + -- Fire StateChanged Event if previousState then self._PreviousState = previousState.Name self.StateChanged:Fire(newState, previousState.Name or "") @@ -548,13 +592,15 @@ end --[=[ Checks if we meet any condition to change the current state. - If any of the transitions returns true then we should change the current state + + The first transition to return true then will change the current state @private @return () ]=] function StateMachine:_CheckTransitions(): () + -- Check every Transition for a possible State change (prioritizing the first found) for _, transition: Transition in self:_GetCurrentStateObject()._transitions do if transition:CanChangeState(self:GetData()) and transition:OnDataChanged(self:GetData()) then self:ChangeState(transition.TargetState) @@ -575,11 +621,31 @@ end @return () ]=] function StateMachine:_CallTransitions(state: State, methodName: string, ...: any): () + -- Call the method for each Transition for _, transition: Transition in state._transitions do task.spawn(transition[methodName], transition, ...) end end +--[=[ + Calls the corresponding method for the given state. (to be cleaned up later) + + @param state State + @param methodName string + @param shouldDefer boolean? + @param ... any + + @private + + @return () +]=] +function StateMachine:_CallMethod(state: State, shouldDefer: boolean, methodName: string, ...: any): () + local action = shouldDefer and "defer" or "spawn" + + self._stateTrove:Add( + task[action](state[methodName], state, ...) + ) +end export type RobloxStateMachine = typeof(StateMachine) export type State = State.State export type Transition = Transition.Transition