Hello!
I am working on a horror game with a friend of mine as the scripter and programmer and wanted feedback on the StateSystem I created. I am new to development and was looking if there is any way I can improve this or should I redesign it. Currently it’s in an infancy stage of development since this is a new project but is the most complex working system I have created so far.
Here are all the scripts that go to the system as well as any data that could help.
StateManager: located in ServerScriptService that acts as a logic and redirects
local PlayerState = {} -- Services local players = game:GetService("Players") local replicatedstorage = game:GetService("ReplicatedStorage") local serverstorage = game:GetService("ServerStorage") local runservice = game:GetService("RunService") local packet = require(replicatedstorage:WaitForChild("Packet")) local GetService = require(replicatedstorage:WaitForChild("Services"):WaitForChild("GetService")) -- Folders local shared = replicatedstorage:WaitForChild("Shared") local assets = replicatedstorage:WaitForChild("Assets") local Services = replicatedstorage:WaitForChild("Services") local StateValues = shared:WaitForChild("StateValues") -- Modules local StateResolver = require(script.StateResolver) local StateDictionary = require(StateValues:WaitForChild("StateDictionary")) local ActionDictionary = require(StateValues:WaitForChild("ActionDicitonary")) -- Remotes local ActionRequest = packet("Request", packet.Any, packet.Any) local ActionExecute = packet("Execute", packet.String, packet.Any, packet.Any, packet.Any) local DeveloperState = packet("DeveloperState", packet.Any) -- Actions local actions = assets:WaitForChild("Actions") local PlayerSprint = require(actions:WaitForChild("PlayerSprint")) -- Extras --// Initialize //-- -- adds player to the state table players.PlayerAdded:Connect(function(player) if PlayerState[player] == nil then PlayerState[player] = {} PlayerState[player].Actions = {} PlayerState[player].HighestPriority = 0 -- means no current state or idle PlayerState[player].Cutscene = false PlayerState[player].CurrentCamera = 0 end local character = player.Character or player.CharacterAdded:Wait() local head = character:WaitForChild("Head") local faceControls = head:WaitForChild("FaceControls", 5) if faceControls then faceControls:Destroy() end end) -- removes player from the state table, saves memory players.PlayerRemoving:Connect(function(player) if PlayerState[player] then PlayerState[player] = nil end end) --[[ extra info CurrentCamera - im gonna forget this so here 1 = free/normal camera 2 = thrid person mouse lock (just like a shift lock) 3 = first person 10 = unlocked mouse (locks camera and all movement) ]] -- checks the action for either "OnExit" or "OnEnter" depending on key local function CheckAction(player, state, key) if state == nil then error("action does not exist") return end if key == "InputBegan" then if state["OnEnter"] then state.OnEnter(player, player.Character, player.Character.Humanoid) PlayerState[player].Actions[state["Name"]] = state PlayerState[player].HighestPriority = state.Priority return true else return false end elseif key == "InputEnded" then if state["OnExit"] then state.OnExit(player, player.Character, player.Character.Humanoid) -- Remove the state from Actions and reset priority PlayerState[player].Actions[state["Name"]] = nil PlayerState[player].HighestPriority = 0 return true else return false end else print(key) error("invaild check given") end end --// Functions //-- -- updates player values in PlayerState table local function StateUpdate(player : Player, action, key : string, active : string) local PlayerUpdate = PlayerState[player] if key == "dev" then -- dev state update for name, data in pairs(PlayerState[player].Actions) do -- ends state CheckAction(player, data, "InputEnded") PlayerState[player].Actions[name] = nil end -- updates highest priority to turn on and off dev state if PlayerUpdate.HighestPriority == 0 then PlayerUpdate.HighestPriority = 20 else PlayerUpdate.HighestPriority = 0 end return elseif key == "update" then -- normal state update for name, data in pairs(PlayerState[player].Actions) do -- executes any code when the state is ended CheckAction(player, data, "InputEnded") PlayerState[player].Actions[name] = nil end return end end -- checks request, resolves request, and executes request local function NewState(player, character, humanoid, input : any, active : any) local state = ActionDictionary[input.Name] local action = StateDictionary.States[state] local movementAction = ActionDictionary.Movement[input.Name] -- these check stuff and end the script if needed if movementAction ~= nil then return end if action == nil then warn("state does not exist") return end -- Handle exiting states differently - don't check priority resolver if active == "InputEnded" then -- Only end the state if it's actually active if PlayerState[player].Actions[action["Name"]] then CheckAction(player, action, "InputEnded") end return action end -- For entering new states, check priority if StateResolver.ResolveRequest(action, PlayerState[player]) == "Fail" then return end StateUpdate(player, action, "update", active) CheckAction(player, action, active) return action end --// Developer Tools //-- -- allows developer to change highestpriority state, allows testing priority and state saving via list function DeveloperState(player : Player, active : any) if active == "InputEnded" then return end -- makes this toggle StateUpdate(player, nil, true) -- sets highest priority to either 20 or 0 but the key is what allows that end --// Main //-- -- basically just uis.inputbegan but adds in packet for client communication ActionRequest.OnServerEvent:Connect(function(player : Player, input : any, active : any) local character = player.Character local humanoid = character:WaitForChild("Humanoid") -- redirects depending on key and action if input == Enum.KeyCode.B then -- camera change if active == "InputEnded" then return end if PlayerState[player].CurrentCamera ~= 2 then PlayerState[player].CurrentCamera += 1 else PlayerState[player].CurrentCamera = 0 end ActionExecute:FireClient(player, "MouseLock", PlayerState[player].CurrentCamera, active) elseif input == Enum.KeyCode.X then -- dev state for testing priority and ending actions DeveloperState(player, active) elseif input == Enum.KeyCode.Z then -- prints entire playerstate for player if active == "InputEnded" then return end print(PlayerState[player]) else -- if it gets here it will attempt a real action like sprint or crouch local action = NewState(player, character, humanoid, input, active) if action == nil then return end if ActionExecute.Id == nil then -- checks if packet exists on server-side error("Packet does not exist on server side") else local TotalActions = GetService:GetTableLen(PlayerState[player].Actions) ActionExecute:FireClient(player, "Update", TotalActions, action["Name"], active) end end end)Also here is my StateResolver that the script above calls for, just a module located in the script
local StateResolver = {} -- Services local replicatedstorage = game:GetService("ReplicatedStorage") -- Folders local shared = replicatedstorage:WaitForChild("Shared") -- Modules local StateDictionary = require(shared:WaitForChild("StateValues"):WaitForChild("StateDictionary")) --// Functions //-- function StateResolver.ResolveRequest(action, PlayerState) if PlayerState == nil then error("did not get player state") return "Failure" end local activeStates = PlayerState.Actions if activeStates and activeStates[action["Name"]] == nil then if action.Priority > PlayerState.HighestPriority then return "Success" else return "Fail" end else return "Success" end end return StateResolver
ActionExecute: located in StarterPlayerScripts, it acts as a server-to-client executor for anything the server can’t do
-- Services local players = game:GetService("Players") local replicatedstorage = game:GetService("ReplicatedStorage") local runservice = game:GetService("RunService") local player = players.LocalPlayer local character = player.Character or player.CharacterAdded:Wait() local humanoid = character:WaitForChild("Humanoid") local packet = require(replicatedstorage:WaitForChild("Packet")) -- Folders local Assets = replicatedstorage:WaitForChild("Assets") local Shared = replicatedstorage:WaitForChild("Shared") local StateValues = Shared:WaitForChild("StateValues") -- Modules local AnimationHandler = require(character:WaitForChild("AnimationHandler")) local CameraHandler = require(character:WaitForChild("CameraHandler")) local StateDictionary = require(StateValues:WaitForChild("StateDictionary")) local ActionDictionary = require(StateValues:WaitForChild("ActionDicitonary")) -- Remotes local ActionExecute = packet("Execute", packet.String, packet.Any, packet.Any, packet.Any) local SettingsCall = packet("SettingsUpdate", packet.Any, packet.Any, packet.Any, packet.Any) --// Execute //-- ActionExecute.OnClientEvent:Connect(function(call, CameraInfo, state, Active) if call == "MouseLock" then CameraHandler.LockMouse(player, character, CameraInfo, Active) elseif call == "Update" then local action = StateDictionary.States[state] CameraHandler.Update(player, action, CameraInfo, Active) end end)
ClientHandler: located in StarterCharacterScripts, it acts as another redirect for input and other information only the client can get
-- Services local uis = game:GetService("UserInputService") local replicatedstorage = game:GetService("ReplicatedStorage") local players = game:GetService("Players") local runservice = game:GetService("RunService") local packet = require(replicatedstorage:WaitForChild("Packet")) -- Folders local Assets = replicatedstorage:WaitForChild("Assets") local Actions = Assets:WaitForChild("Actions") local Animations = Assets:WaitForChild("Animations") -- Modules local AnimationHandler = require(script.Parent:WaitForChild("AnimationHandler")) local AnimationIndex = require(Animations:WaitForChild("AnimationIndex")) -- Remotes local ActionRequest = packet("Request", packet.Any, packet.Any) local DeveloperState = packet("DeveloperState", packet.Any) local AnimationExecute = packet("ActionExecute", packet.Any) --// Redirects //-- -- redirects the animation to the AnimationHandler from server state change AnimationExecute.OnClientEvent:Connect(function(action) --local animation = AnimationIndex[action] --AnimationHandler.Execute[action](animation) end) --// Functions //-- -- fires server (thank you @guestw3334) local function SendInput(input, active) if input.UserInputType == Enum.UserInputType.Keyboard then ActionRequest:Fire(input.KeyCode, active) end end --// Inputs //-- -- reads for input began uis.InputBegan:Connect(function(input) if ActionRequest.Id == nil then -- checks if packet exists on server-side error("Packet does not exist on server side") else -- if exists then fire SendInput(input, "InputBegan") end end) -- reads for input ended uis.InputEnded:Connect(function(input) if ActionRequest.Id == nil then -- checks if packet exists on server-side error("Packet does not exist on server side") else -- if exists then fire SendInput(input, "InputEnded") end end)
CameraHandler: located in StarterCharacterScripts, it acts as a service module for the client act on the camera based on the input from the server or user
local CameraHandler = {} -- Services local players = game:GetService("Players") local replicatedstorage = game:GetService("ReplicatedStorage") local runservice = game:GetService("RunService") local uis = game:GetService("UserInputService") local tween = game:GetService("TweenService") local camera = workspace.CurrentCamera local player = players.LocalPlayer local character = player.Character or player.CharacterAdded:Wait() local root = character:WaitForChild("HumanoidRootPart") local humanoid = character:WaitForChild("Humanoid") -- Folders local Assets = replicatedstorage:WaitForChild("Assets") local Shared = replicatedstorage:WaitForChild("Shared") local StateValues = Shared:WaitForChild("StateValues") local Configs = Shared:WaitForChild("Configs") -- Modules local ActionDictionary = require(StateValues:WaitForChild("ActionDicitonary")) local StateDictionary = require(StateValues:WaitForChild("StateDictionary")) local DefaultConfig = require(Configs:WaitForChild("DefaultConfig")) -- Variables local Previous = 0 local Enums = { ["0"] = { ["MouseBehavior"] = Enum.MouseBehavior.Default, ["CameraMode"] = Enum.CameraMode.Classic, ["AutoRotate"] = true, ["Subject"] = "Humanoid" }, ["1"] = { ["MouseBehavior"] = Enum.MouseBehavior.LockCenter, ["CameraMode"] = Enum.CameraMode.Classic, ["AutoRotate"] = true, ["Subject"] = "Humanoid" }, ["2"] = { ["MouseBehavior"] = Enum.MouseBehavior.LockCenter, ["CameraMode"] = Enum.CameraMode.LockFirstPerson, ["AutoRotate"] = true, ["Subject"] = "Head", OnEnter = function() for _, v in pairs(character:GetChildren()) do if v:IsA("BasePart") then v.LocalTransparencyModifier = 0.7 elseif v:IsA("Accessory") then v.Handle.Transparency = 1 elseif v.Name == "Head" then local face = v.face face.Transparency = 1 end end end, OnExit = function() for _, v in pairs(character:GetChildren()) do if v:IsA("BasePart") then v.LocalTransparencyModifier = 0 elseif v:IsA("Accessory") then v.Handle.Transparency = 0 elseif v.Name == "Head" then local face = v.face face.Transparency = 0 end end end } } --// Functions //-- --[[ extra info CurrentCamera - im gonna forget this so here 0 = normal camera without lock 1 = free/normal camera 2 = third person mouse lock (just like a shift lock) 3 = first person 10 = unlocked mouse (locks camera and all movement) ]] --[[ local _, y = workspace.CurrentCamera.CFrame.Rotation:ToEulerAnglesYXZ() --Get the angles of the camera root.CFrame = CFrame.new(root.Position) * CFrame.Angles(0,y,0) ]] -- mouse lock updates function CameraHandler.LockMouse(player : Player, character : Model, CameraInt : IntValue, Active : string) if CameraInt == nil then error("cameraType Int == nil") return end local Previous local CameraType if CameraInt == 0 then Previous = tostring(2) CameraType = tostring(CameraInt) else Previous = tostring(CameraInt - 1) CameraType = tostring(CameraInt) end print(Previous) print(CameraType) uis.MouseBehavior = Enums[CameraType]["MouseBehavior"] player.CameraMode = Enums[CameraType]["CameraMode"] humanoid.AutoRotate = Enums[CameraType]["AutoRotate"] camera.CameraSubject = character:WaitForChild(Enums[CameraType]["Subject"]) if Enums[Previous]["OnExit"] then Enums[Previous]["OnExit"]() else print("no OnExit for cameratype") end if Enums[CameraType]["OnEnter"] then Enums[CameraType]["OnEnter"]() end end function CameraHandler.Update(player, action, ActiveStates, active) local FOVtween if active == "InputBegan" then FOVtween = tween:Create(camera, TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {FieldOfView = action["FOV"]}) else if ActiveStates ~= 0 then print("playerstate # == 0") return end print("fov changing, playerstate == " .. ActiveStates) FOVtween = tween:Create(camera, TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {FieldOfView = DefaultConfig.Camera["FOV"]}) end FOVtween:Play() end -- for more specific tweens that require longer code and more detail such as gradual increase function CameraHandler.Tween(player, State) end return CameraHandler
There is more to the system but these are the main few, if I need I can send more.