A minimal NotITG v4.3.0 theme focused on developer simplicity and modern solutions.
Gimmick is meant to be partly a playground, and partly an example for other developers aiming to make a theme with no idea where to begin.
Because I really don't know if any of what I'm doing is going to stick. The stuff presented here is highly experimental and prone to breaking under pressure!
Working with gimmick can be a doozy because of how far away it is from typical theming or even typical Stepmania code. This is why this section exists: to note specific quirks you may encounter.
For actor initialization, gimmick uses actor235, a work-in-progress (mostly complete) port of Uranium Template's actor system. For the most part, you can refer to its documentation, but it won't always align since actor235 has been greatly modified and rewritten to fit working within a theme.
This section will note the differences between actor235 and Uranium Template's actor initialization, aswell as quirks to look out for that are relevant within gimmick.
actor235 introduces the concept of contexts. A context, abstractly speaking,
is some context in which actors can be initialized. Actors cannot be
initialized outside of a context, as there's no way for you to load an actor
during runtime, so you will need a Context
in order to initialize actors.
In gimmick, Context
s are created during screen initialization, and are only
usable in said screen initialization. Usually this means that you'll be given
a Context
whenever you define a screen:
gimmick.ActorScreen('CoolScreen', function(self, ctx, scope)
-- `ctx` here is your Context
local quad = ctx:Quad()
end)
You can only define actors through a Context
:
local quad = ctx:Quad()
local sprite = ctx:Sprite(...)
local shader = ctx:Shader(...)
-- and so on, and so forth
ActorFrame associations are also always handled through a Context
:
local frame = ctx:ActorFrame()
local child = ctx:Quad()
ctx:addChild(frame, child)
Do not try and store the Context
, as after the function you're given it in
passes and initialization completes, it is locked and accessing it will error.
-- DON'T DO THIS
local quad = ctx:Quad()
quad:addcommand('Init', function()
local otherQuad = ctx:Quad()
end)
-- NO NO
local storedContext
return function(ctx)
storedContext = ctx
end
Unlike Uranium Template, actor235 does not expose any functions to work with
actors to account for their proxied nature (such as setShader
and similar).
Instead, you are (for the time being) entrusted with taking care of proxied
actors yourself.
To access the raw actor of a proxied actor, import actor235
and use the
Proxy
module:
local actor235 = require 'gimmick.lib.actor235'
local shader = ctx:Shader('Shaders/penis.frag')
actor235.Proxy.getRaw(shader) --> RageShaderProgram or nil
-- nil is returned when it is not yet initialized
-- You could use this, for instance, like this:
sprite:SetShader(actor235.Proxy.getRaw(shader))
-- However, keep in mind InitCommands will also return the raw actor for a
-- simpler way to achieve the same:
shader:addcommand('Init', function(a)
sprite:SetShader(a)
end)
-- !! BE WARY OF LOAD ORDER !!, because `shader`'s InitCommand here could be ran
-- before `sprite`'s, depending on the order in which they're defined.
ActorFrame transforms only apply to children when the children are drawn within the ActorFrame's drawfunction. Drawing the ActorFrame's children outside of its drawfunction, or drawing other ActorFrames' children in another drawfunction should be considered undefined behavior to avoid discovering weird issues down the line.
local quad = ctx:Quad()
quad:xywh(0, 0, 32, 32)
local frame = ctx:ActorFrame()
frame:xy(100, 100)
ctx:addChild(frame, quad)
local parentlessQuad = ctx:Quad()
parentlessQuad:xywh(0, 0, 32, 32)
self:SetDrawFunction(function()
-- **Undefined behavior**, but should render at x0 y0
quad:Draw()
-- OK, renders at x0 y0
parentlessQuad:Draw()
end)
frame:SetDrawFunction(function()
-- OK, renders at x100 y100
quad:Draw()
-- **Undefined behavior**, but should render at x100 y100
parentlessQuad:Draw()
end)
Alongside contexts, you'll be passed along a Scope
during initialization. A
scope is a generalization of everything specific to a screen. Inside it is:
- A
tick
instance, letting you use Mirin-like scheduling within the screentick:func
,tick:perframe
etc will behave similarly to their Mirin equivalentstick:aux()
will create the equivalent of adefinemod
tick:easable()
will create aneasable
instance that is updated and cleant up automatically
- An
event
instance, letting you register event handlers- Any events called globally will be passed along; see: Global events
- Any events called will only propogate within the screen
- Additionally, you have access to
on
andoff
events, called when the screen's OnCommand and OffCommand are triggered
Global events can be listened to and created with the event
global listener,
or listened to with any event
instances in a scope (preferable to
use when possible).
A raw keypress. device
corresponds to an InputDevice
. For instance to detect
keyboard presses, do:
event:on('keypress', function(device, key)
if device == InputDevice.Key then
-- ...
end
end)
For keyboards, reference RageKeySymToString
for possible values of key
. You can likely find the other possible values of
key
for other device types within the same file.
Same as keypress
, except triggered when the key is no longer held.
A processed button press, corresponding to the actions you can bind in the game. See m_szButtonNames for possible values.
Same as release
, except triggered when the key is no longer held.
Triggered whenever the window resizes. Uses the display resolution instead of the usual screen resolution. Common usecase is to recreate AFTs with fresh sizes.
A warning; not necessarily a user-facing one. Calls to the warn
global will
send this event. Currently only displayed in the console, but could potentially
be shown to the user later on.
print(...)
works the same way as in regular Lua, except that every value is passed throughpretty
. It also callsDebug
with the results, which means it'll show up in stdout withoutShowLogOutput=1
; useful for decluttering the NotITG logs during debugging.warn(msg: string)
displays a message in the console.pretty(a: any)
is our in-house Lua pretty-printer. Also handles actors.actorToString(a: Actor)
tries to give a fairly accurate XML representation of an actor tree.introduceEntropyIntoUniverse()
introduces entropy into the universe. Feel free to call this at any point if you wish for that to happen.
Make sure to look through util.lua for the rest; there's plenty of handy things to be found there.
Folders will sometimes have readme.md
files in them documenting how they're
meant to be used. Be sure to read those for full details on where things should
go.
- Code should always go in
gimmick/
.Scripts/
should be treated as exclusively an interface to the game, and as such code written there should ideally just refer to code ingimmick/
whenever possible for the highest degree of control. - Graphics should go in
Graphics/
, usually. Whenever assets have special structure, such as being costumizable by the user, consider putting them in a new folder at the root. - Shaders should go in
Shaders/
. This is a made-up folder that's unusued by the actual game, but should be where you store shaders. - XML files required for the game go in
BGAnimations/
andGraphics/
. Use theman
utility at the root of this repository to automatically generate them. Sounds/
andFonts/
should be used as they would be in a regular theme.Mascots/
is a more user-facing folder, storing mascot data and assets.
Folders at the root should be minimized. Folders that are at the root at the
folder not required by Stepmania's theme structure (and aren't gimmick
or
relate to repository structure) are currently considered to be moved to a better
spot later on.
Be sure to try and deduplicate files whenever possible with .redir
s.