Getting Started¶
Welcome to Ignition's tutorial section! Here, you'll learn how to architect code strictly and intuitively with Ignition.
But wait, heed before you proceed...
Ignition is not your friend; it is your supervisor.
If you are looking for a "plug-and-play" utility library to sprinkle into an existing project, turn back now. Ignition is a strict architectural framework that physically enforces the boundaries of your code.
By proceeding, you must be aware that:
-
The Spider is Watching: The
StackSpiderwill crash your thread if you attempt to "cheat" your way into private members from the wrong scope. -
Header-First or Fail: You cannot "duct-tape" logic together. You must define your interface in a
.definitionfile before you are allowed to write a single line of implementation. -
No "Metatable Soup": Ignition kills the "pinky-promise" style of OOP. There are no underscores here; there are only hard access violations and strict type-matching.
-
The
ref()Tax.. and others..: You must manually wrap callbacks inref()when crossing engine boundaries, or the security system will treat your own code as an intruder.
This rigidity is a feature, not a bug. Ignition is designed for large-scale, high-integrity systems where "moving fast and breaking things" is replaced by "designing once and running forever."
If you aren't 100% willing to let a framework dictate your file structure and access patterns for the sake of absolute memory safety and zero-overhead production speed, Ignition is not for you.
What You Need To Know¶
These tutorials assume:
- You are comfortable with Roblox and the Luau scripting language.
- By extension, you are familiar with C++ or Java coding paradigms.
- You have basic understanding on the concepts of Object Oriented Programming.
- You are ready with choosing between eager or lazy linking.
- You are comfortable with losing intellisense when you use lazy linking
-
You are comfortable with wrapping engine level callbacks in
ref(). (1)- You must use
ref()whenever a function is called from a non-scripting thread (likeRunServiceortask.delay) to preventStackSpideraccess violations.
- You must use
Of course, depending on your background, some concepts may feel more alien than others. Ignition is built for professional endurance; the initial friction you feel is simply the framework ensuring your architecture is bulletproof, so don't be discouraged.
Installation¶
You can deploy Ignition into your project using one of the following strategies.
Wally (Recommended)¶
Wally is the standard package manager for professional Luau development.
- Copy
Ignition = "lnconcinnity/ignition@latest"for the latest release. - Paste the string under the
[dependencies]section of yourwally.toml. - Run
wally installin your command line.
Source Code¶
Use this method if you synchronize external files into Roblox Studio via Rojo.
- Download the source code ZIP from the latest release.
- Copy the
srcfolder from the ZIP archive. - Paste the folder into your project's
sharedorlibdirectory. - Rename the folder to
Ignition.
Roblox Model (.rbxm)¶
This is the fastest method for developers working exclusively inside Roblox Studio.
- Download the
Ignition.rbxmfile from the latest release. - Drag and drop the file directly into the Roblox Studio Explorer.
- Place the folder into
ReplicatedStoragefor shared access.
Creator Store (Toolbox)¶
You can add Ignition to your inventory through the Roblox Creator Store.
- Open the Toolbox in Roblox Studio.
- Search for
Ignition, with lnconcinnity filtered as creator, under the Models tab. - Click the model to insert it into your current place.
Quick Start¶
Ignition is a Dependency Injection (DI) framework. Your primary concern is no longer "where the script goes," but how you structure your Solutions.
1. Project Structure¶
Ignition crawls your environment to find folders suffixed with Solution. It then automatically links every module inside them.
Root (ServerScriptService / StarterPlayerScripts)
└── GameSolution
└── Player
├── Player.definition <-- The "Interface" (Header)
└── Player <-- The "Logic" (Source)
2. Creating a class¶
Ignition enforces a strict separation between what a class is and what a class does.
This is an example of a .definition file and how it's structured, this is your contract. It defines public and protected members.
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Ignition = require(ReplicatedStorage.Ignition)
local class, property, abstract, func, FLAGS =
Ignition.class, Ignition.property, Ignition.abstract, Ignition.func, Ignition.FLAGS
return abstract (class "BaseRemote" {
public = {
Name = property {
from = "shared",
flags = { FLAGS.Readonly },
type = "string",
value = nil
},
},
protected = {
Send = func {
from = "shared",
dummy = (nil :: any) :: (self: any, options: {
targets: { Player },
unreliable: boolean,
args: { [number]: any, n: number },
}) -> (),
},
},
})
The implementation file is your source. You use import() to bind your logic to the definition.
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Ignition = require(ReplicatedStorage.Ignition)
local import = Ignition.import
local BaseRemoteHeader = require(script.Parent["BaseRemote.definition"])
local BaseRemote = import(BaseRemoteHeader)
type BaseRemoteInstance = typeof(BaseRemote)
function BaseRemote:Send(options)
-- Logic for sending data
end
-- The Constructor
BaseRemote(function(self: BaseRemoteInstance, name: string)
self.Name = name
end)
return BaseRemote(true) -- mark it as complete
Type Integrity Warning
This example eagerly loads the interface by directly requiring the header (which allows the type solver to properly infer the class object thanks to traceability). While Ignition allows lazy loading by leaving import() empty, doing so causes you to lose ALL type inferences (Types will return any instead).
Design your architecture accordingly.
Tip
Skip the hassle of having to manually define either both of your header/source files by using these QoL snippets for VSCode!
Do note that the snippet for implementation/source files are on EAGER, which risks circular dependencies, but who in their right mind would circularly reference the header and source files?
3. System Configuration (Optional)¶
Before the system is live, you can modify the Configuration file within the Ignition core. This defines where the framework looks for code and what it ignores.
-- Ignition/Configuration
return {
VERSION = version,
-- Defines where Ignition starts crawling for Solutions
ROOT = if game:GetService("RunService"):IsServer()
then game:GetService("ServerScriptService")
else game:GetService("Players").LocalPlayer:WaitForChild("PlayerScripts"),
SHARED_ROOT = game:GetService("ReplicatedStorage"),
-- Names that Ignition will never crawl
EXCLUDES = set("assets", "templates", "prefabs", "temp"),
-- The suffix required for a folder to be treated as a Solution
SOLUTIONS_KEY = "Solution",
}
Tip
Modifying the ROOT allows you to restrict Ignition to specific sub-folders, reducing the crawling overhead in massive projects.
4. Igniting the Engine¶
The final step is the initialization. This is done by require-ing the Ignition module and calling it as a function.
Override Definition Extensions¶
By default, Ignition looks for .definition files. However, you can pass a string argument during initialization to override the expected file extension (e.g., if you prefer .interface or .header, or more efficiently: .d).
-- main.server.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- 1. Initialize with an optional extension override
-- 2. This begins the Linker sequence across ROOTS and SHARED_ROOT
local Ignition = require(ReplicatedStorage.Ignition)(".d")
-- And that's it!
local new = Ignition.new
local baseRemote = new "BaseRemote"("Message")
print(baseRemote.Name) -- Message
baseRemote.Name = "SendMessage" -- ERRORS (Since it's a readonly property, you cannot set it once outside the constructor!)
If no errors were raised, the system is now operational. Any class defined in your solutions is now globally accessible and architecturally enforced.
Troubleshooting¶
Why can't I instantiate a class with new?
The most common reason for this is that your header/source files were not indexed by Ignition. Verify that:
- Your files are placed inside a folder with the
Solutionkeyword at the end (e.g.,GameSolution). - Your files are not located within folders defined in
Configuration.EXCLUDES. - You have called the Ignition module as a function
require(Ignition)()to start the crawl.
Common Pitfalls¶
-
Incorrect Extension Overrides
If you passed an override to the Ignition constructor (e.g.,
require(Ignition)(".d")), but your files are named.definition, Ignition will ignore them. Ensure your file extensions match your configuration exactly. -
Implementation Errors vs. Definition Validity Because Ignition separates the two, an error in your implementation file (syntax error, logic crash) will not prevent the definition from being registered.
- The Result: You can still call
new(targetClass)(...)on the class, but the logic will fail or appear "empty" because the implementation never successfully bound itself to the header viaimport().
- The Result: You can still call
-
Lazy Loading Deadlocks Ignition supports lazy loading by passing a string to
newinstead of the actualInterfaceobject. While flexible, this can lead to silent failures:- If the class name is misspelled or the file hasn't been crawled yet, the
Registrywill yield the thread while it waits for that class to exist.- Though on later versions of Ignition now has
Registrywith an internal timeout handler, it is best advised to look into your code structure as this may very well be a code smell.
- Though on later versions of Ignition now has
- If the class name is misspelled or the file hasn't been crawled yet, the
Under the Hood: The Registry¶
The Registry is the source of truth for all active interfaces. It uses coroutine.yield() to manage dependencies, ensuring that one class doesn't attempt to inherit from or link to another class that isn't ready.
The Registry also handles the cold-loading deep inheritance chains (e.g., Child propagates upward to Grandparent first, then descends back from Grandparent to Child).
Warning
The Registry strictly forbids calls from scripts outside the Class scope to prevent developers from "manually" injecting fake classes into the framework. Thus, the Registry is architecturally a blackbox.