I do it this way too… which makes it a bit more nuanced than the dogmatic rule would suggest. Using an Object Oriented approach really helps in this regard, since it allows you to have a bunch of extensive well defined blocks of functionality in classes, and then “hook them together” using your one bootstrap script.
Here's the "main" class for my current project for an idea of how I do it:
local GameState = require(game.ReplicatedStorage.GameState)
local GameController = require(game.ReplicatedStorage.GameController)
local GameView = require(game.ReplicatedStorage.GameView)
local NetmapView = require(game.ReplicatedStorage.NetmapView)
local UnitInfoView = require(game.ReplicatedStorage.UnitInfoView)
local LocalPlayerData = require(game.ReplicatedStorage.LocalPlayerData)
local Places = require(game.ReplicatedStorage.Places)
local Netmap = require(game.ReplicatedStorage.Netmap)
local SoundManager = require(game.ReplicatedStorage.SoundManager)
local DialogueView = require(game.ReplicatedStorage.DialogueView)
local WarezView = require(game.ReplicatedStorage.WarezView)
local Scripts = require(game.ReplicatedStorage.Scripts)
local WindowsButton = require(game.ReplicatedStorage.WindowsButton)
local MainMenuView = require(game.ReplicatedStorage.MainMenuView)
local UserInputService = game:GetService('UserInputService')
local UglyTutorialNonsense = require(script.UglyTutorialNonsense)
local MainView = {}
-- Can't use the proper API because I don't want to wait... ARGHH
local ExtraY = 36 --game:GetService('GuiService'):GetGuiInset()
function MainView.getSize()
local Mouse = game.Players.LocalPlayer:GetMouse()
--if UserInputService.TouchEnabled then
return UDim2.new(0, math.min(800, Mouse.ViewSizeX), 0, math.min(600, Mouse.ViewSizeY + 2*ExtraY))
end
function MainView.new()
local this = {}
-- Make root GUI
local mGui = Instance.new('Frame')
mGui.BackgroundColor3 = Color3.new(0, 0, 0)
mGui.Size = MainView.getSize()
mGui.Position = UDim2.new(0.5, 0, 0.5, -ExtraY/2)
mGui.AnchorPoint = Vector2.new(0.5, 0.5)
mGui.BorderSizePixel = 0
local topFill = Instance.new('Frame')
topFill.Name = 'TopFill'
topFill.BackgroundColor3 = Color3.new(0, 0, 0)
topFill.Position = UDim2.new(0, 0, 0, -ExtraY)
topFill.Size = UDim2.new(1, 0, 0, ExtraY)
topFill.BorderSizePixel = 0
--topFill.Parent = mGui
function this:GetGui()
return mGui;
end
-- Make menu
local mMainMenu = MainMenuView.new(mGui)
local mMenuButton = script.MenuButton:Clone()
WindowsButton.new(mMenuButton)
mMenuButton.Parent = mGui
mMenuButton.MouseButton1Click:connect(function()
print("Open main menu")
mMainMenu:Show()
end)
-- Make netmap
local mNetmapView = NetmapView.new()
mNetmapView:GetGui().Position = UDim2.new(0.5, 0, 0.5, 0)
mNetmapView:GetGui().Parent = mGui
-- Give the netmap a unitInfoView
local mUnitInfo = UnitInfoView.new(mNetmapView:GetGui(), LocalPlayerData:GetProgramList())
mUnitInfo:SetProgramListVisible(false)
mUnitInfo:ClearSelectedUnit()
-- Dialogue
local mDialogue = DialogueView.new()
mDialogue:SetVisible(false)
mDialogue:GetGui().Parent = mGui
-- Setup events
mNetmapView.NodeSelected:connect(function(nodeId)
if LocalPlayerData:CanAccessNode(nodeId) then
local node = Netmap.ById[nodeId]
if node.Id == 'hq' then
-- Special behavior
elseif node.Warez then
-- Visit warez node
this:VisitWarez(nodeId)
else
-- Do the battle
this:PlayGame(node.PlaceId, nodeId)
end
end
end)
-- Warez node
function this:VisitWarez(nodeId)
this:ProcessWonBattle(nodeId, 0) -- make it visible / beaten
-- Show the GUI and handle the events for it
local warezGui = WarezView.new(nodeId, Netmap.ById[nodeId].Warez)
warezGui.MadePurchase:connect(function(id)
mNetmapView:UpdateCreditDisplay()
this:ShowNotification("Acquired program "..Scripts[id].Name)
end)
warezGui.Done:connect(function()
warezGui:GetGui().Parent = nil
end)
warezGui:GetGui().Parent = mGui
end
-- Play the tutorial dialogue and tutorial
function this:PlayTutorial()
UglyTutorialNonsense:PlayTutorial(mGui, mNetmapView, mDialogue)
this:ProcessWonBattle('hq', 1000)
game.ReplicatedStorage.Remotes.BeatTutorial:FireServer()
end
-- Play a game at a place
function this:PlayGame(placeId, nodeId)
local placeData = Places[placeId]
if not placeData then
error("Missing place "..placeId)
end
local gameState = GameState.new(placeData, LocalPlayerData:GetProgramList(), GameState.ClientDelayFunc)
local gameController = GameController.new(gameState)
local gameView = GameView.new(gameState, gameController)
-- Restore the main state when the game is over
gameView.CloseGame:connect(function(didWin, replay)
mNetmapView:GetGui().Visible = true
SoundManager:Play('MainBackgroundLoop')
gameView:Destroy()
-- Did we win?
if didWin then
this:ProcessWonBattle(nodeId, gameState:GetCreditsEarned())
end
end)
gameView:getGui().Position = UDim2.new(0.5, 0, 0.5, 0)
gameView:getGui().Parent = mGui
mNetmapView:GetGui().Visible = false
SoundManager:Stop('MainBackgroundLoop')
end
-- We won a battle, process the node revealing, and
-- play through the win-triggers for that node
function this:ProcessWonBattle(nodeId, creditsCollected)
-- Get credits
LocalPlayerData:AddCredits(creditsCollected)
mNetmapView:UpdateCreditDisplay()
-- Did we already beat this node?
if LocalPlayerData:HasBeatenNode(nodeId) then
return -- Don't update the node or play the win triggers again
end
-- Update the state
LocalPlayerData:SetNodeBeaten(nodeId)
mNetmapView:SetNodeBeaten(nodeId)
-- Handle the win-triggers
local conversation = Netmap.ById[nodeId].Conversation
if conversation then
-- Show it
mDialogue:SetVisible(true)
mDialogue:ExecuteConversation(conversation)
mDialogue:SetVisible(false)
-- Process the result
if conversation.Function then
local f = conversation.Function
if f.Type == 'revealNode' then
LocalPlayerData:RevealNode(f.Id)
mNetmapView:SetNodeVisible(f.Id)
mNetmapView:HighlightNode(f.Id)
elseif f.Type == 'upgradeSecurity' then
LocalPlayerData:SetSecurityLevel(f.Level)
-- We need to call again to make sure that the nodes that are now accessible appear that way
mNetmapView:SetNodeBeaten(nodeId)
this:ShowNotification("Upgraded security level to "..f.Level)
elseif f.Type == 'getProgram' then
LocalPlayerData:AddUnit(f.Id)
this:ShowNotification("Received program: "..Scripts[f.Id].Name)
elseif f.Type == 'getCredits' then
LocalPlayerData:AddCredits(f.Amount)
mNetmapView:UpdateCreditDisplay()
this:ShowNotification("Received credits: "..f.Amount)
elseif f.Type == 'beginNightfall' then
LocalPlayerData:SetSecurityLevel(5)
mNetmapView:SetNodeBeaten('ph45') -- Special case, do this to show the access to the boss node
-- TODO:
elseif f.Type == 'endNightfall' then
-- TODO:
else
error("Bad function type: "..tostring(f.Type))
end
end
end
end
local NotificationBoxTween = TweenInfo.new(1.3, Enum.EasingStyle.Quint, Enum.EasingDirection.Out, 0, true)
function this:ShowNotification(text)
local box = script.NotificationBox:Clone()
box.Inset.Content.Text = text
box.Parent = mNetmapView:GetGui()
local TweenService = game:GetService('TweenService')
local inAnim = TweenService:Create(box, NotificationBoxTween, {
Position = UDim2.new(1, -24, 1, -10);
})
inAnim:Play()
inAnim.Completed:connect(function()
box:Destroy()
end)
end
SoundManager:Play('MainBackgroundLoop')
local function handleError(title, body)
local gui = script.ErrorBox:Clone()
gui.Content.Text = title.."\n"..body
gui.CloseButton.MouseButton1Click:connect(function()
gui:Destroy()
end)
gui.Parent = mGui
end
game:GetService('LogService').MessageOut:connect(function(message, messageType)
if messageType == Enum.MessageType.MessageError then
handleError("A CLIENT ERROR OCURRED - Please screenshot this and send it to Stravant", message)
end
end)
game.ReplicatedStorage.Remotes.ServerError.OnClientEvent:connect(function(message)
handleError("A SERVER ERROR OCURRED - Please screenshot this and send it to Stravant", message)
end)
if not LocalPlayerData:HasBeatenNode('hq') then
spawn(function()
this:PlayTutorial()
end)
end
return this
end
return MainView
Absolutely. These are the worst thing I routinely see people doing on Roblox, it totally violates the contract of what a ModuleScript is supposed to be, there is no reason for these to not either use Initialization functions or just be normal scripts.
Yes, you should obviously start them in the initialize function using Spawn()
. You could even have an entirely separate “initialization step” of Run()
calls on your Modules that need it for starting your long running loops.