tier
layered game framework
v2.0
Download
Module:
Example place, includes my suggested setup:
(I recommend looking through this!)
Structure
tier consists of 3 layers of modules:
- systems (uses)
- managers (uses and used)
- providers (used)
This is to create a clear separation of concerns and modularity. They all follow “rules” about how they interact with other layers.
Rules
The framework does not force you to follow any rules. It’s up to you to ensure your modules follow the rules/structure of the framework.
Generally modules only require down the tree (systems > managers > providers). The only exception is Managers can require other Managers.
-
systems: have no public functionality and should not be required by other modules. Their purpose is to use the Managers and Providers to handle any logic
-
managers: can require other Managers and also require Providers. Their purpose is to manage more specific game functionalities
-
providers: have only public functionality and should not require other modules in the framework. They are foundational modules with modular functionality.
Lifecycle
- call
tier.prepare
Server does some checks to make sure things are set up correctly.
Client will yield until the server finishes loading (can be changed for client-only games).
- Add your modules
Add all your modules to the framework via tier.gather
or tier.gatherFromFirstFolders
(or individually if choose)
- call
tier.begin
tier will consecutively move through it’s stages, requiring the respective modules for the stage and their respective startup functions. Each layer’s startup functionality can be found in the “Module Startup” section.
Here are the stages the framework goes through:
Module Startup
-
systems
Do not have any startup functions -
managers
The framework will check for a.Start
. This will only be called after all the modules dependencies have started. Check out the ‘dependency injection’ for how to use this. -
providers
The framework will check for a.Start
. This is not exactly important and does not serve much purpose, but it’s available for people who prefer it.
Dependency Injection
- Only Managers have dependency injection!
Since Managers can require other Managers, they have a dependency injection system. This ensures there’s no race conditions where a Manager tries to use one of its dependencies before it has loaded.
To use dependency injection, add a “Needs” table to your module, and include and modules it depends on. Example:
local CoolClientManager = require(Managers.CoolClientManager)
local Manager = {
Needs = {CoolClientManager} -- Will make sure dependencies load first
}
Managers also have an optional .Start
which is only called after all of its dependencies have been started. Keep in mind dependency injection is optional.
Organization Examples
-
A camera module = fits into a Provider (CameraProvider). Why? The module offers foundational and modular functionality that will be used by other modules. The CameraProvider has no reason to be requiring other modules in the framework.
-
An effect module = fits into a Manager (EffectManager). Why? The module offers public functionality that could possibly be used by other Managers or Systems, but could possibly need to require the CameraProvider for effects.
-
A game loop module = fits into a System (GameLoopSystem). Why? The module handles the core loop of the game. It requires multiple different Managers and Systems to do things at certain times. This module offers no public functionality as its only purpose is to keep things running in a loop.
API
tier.getStage(): (string?, number?)
-- returns the name of the stage, and the stage index number. Will return nil, nil if the framework has not been prepared yet.
tier.prepare()
-- prepares the framework
tier.gather(From: Instance, Deep: boolean, Add: (ModuleScript) -> (), Filter: ((ModuleScript) -> (boolean?))?)
-- auto gathers modules from a directory (from)
-- "deep" to use GetDescendants instead of GetChildren
-- adds them to the framework using the Add function
-- optional "filter" for filtering modules out of the selection
-- Example: tier.gather(SystemsFolder, false, tier.system, ModuleScriptCallback)
tier.gatherFromFirstFolders(From: Instance, Add: (ModuleScript) -> (), Filter: ((ModuleScript) -> (boolean?))?)
-- auto gathers modules from a directory (from)
-- if a folder is found, it will also gather from that folder (this is not recursive; it only happens for the first layer, hence 'FromFirstFolders')
-- adds them to the framework using the Add function
-- optional "filter" for filtering modules out of the selection
-- this is especially useful for Systems because they have nothing that depends on them and can be easily moved around into folders for organization
tier.provider(ModuleScript: ModuleScript)
-- adds a provider to the framework
tier.manager(ModuleScript: ModuleScript)
-- adds a manager to the framework
tier.system(ModuleScript: ModuleScript)
-- adds a system to the framework
tier.begin()
-- starts the framework
Closing
Let me know your thoughts on my framework! It’s a concept that I’ve been formulating for a while now as I’ve worked with countless other frameworks. It may take a little to get used to, but ultimately synergizes very well with projects of any complexity.
I’m open to any critiques or concerns you may have, just let me know!