ActorScroller & ScreenSelectMaster - RhythmLunatic/stepmania GitHub Wiki

ScreenSelectMaster is a built in screen that's used to display menus such as the title screen, play mode, and style select.

There is also ActorScroller, a similar lua based Actor that behaves mostly the same.

ScreenSelectMaster

This is much easier to implement than ActorScroller but doesn't give you full control.

Pros:

  • Input handling already works
  • Easier and usually quicker to implement.
  • GainFocusCommand and LoseFocusCommand works for any actor in the scroller.

Cons:

  • Can only have one.
  • Limited to executing gamecommands when you press start. (This can kind of be worked around, see ceveats section)

To get started, simply define a new screen then set fallback as ScreenSelectMaster. Here is a basic scroller you can copy and paste:

[ScreenSelectModeCustom]
Fallback="ScreenSelectMaster"
PrevScreen="ScreenTitleMenu"
NextScreen=Branch.StartGame()

#Change these to your own choices! If you need help, check the gamecommand wiki page or look at other themes.
DefaultChoice="Game"
ChoiceNames="Game,Freestyle,Customize"
ChoiceGame="applydefaultoptions;text,GAME MODE;setenv,GameMode,arcade;screen,"..Branch.StartGame()
ChoiceFreestyle="applydefaultoptions;text,FREE STYLE;setenv,GameMode,freestyle;screen,ScreenSelectPlayMode"
ChoiceCustomize="screen,ScreenCustomize;text,CUSTOMIZE"

ShowScroller=true
WrapScroller=true
LoopScroller=true
ScrollerSecondsPerItem=0.2

ScrollerX=SCREEN_CENTER_X
ScrollerY=SCREEN_CENTER_Y
ScrollerTransform=function(self,offsetFromCenter,itemIndex,numItems) \
	self:x(offsetFromCenter * SCREEN_WIDTH); \
end;

ShowIcon=false
UseIconMetrics=false
PerChoiceScrollElement=false
PerChoiceIconElement=false

There's a lot of stuff here, so scroll down to the Glossary and come back to find out what it all means. Or keep reading. Either way is fine.

Now that you've copypasted that into your metrics.ini (probably), you can add graphics for the scroller to display.

The name of the Screen is ScreenSelectModeCustom and PerChoiceScrollElement is false, so all the scroller items will be loaded from Graphics/ScreenSelectModeCustom scroller.

Of course you don't want the same item to be loaded for every choice, so here is the standard trick everyone uses:

  1. Create a folder named "ScreenSelectModeCustom scroller" in Graphics.
  2. Inside the folder's default lua, put this in:
local gc = Var("GameCommand");
return Def.ActorFrame{
    LoadActor(gc:GetName());
}
  1. Create lua files or graphics that match the name of the choice and put them inside the ScreenSelectModeCustom scroller folder (where ScreenSelectModeCustom is the name of your screen, of course). Ex. Freestyle.lua, Customize.lua, Game.lua.
  2. I don't know where else to put this so I'm putting it here. All children actors of a ScreenSelectMaster derived screen can use GainFocusCommand and LoseFocusCommand. These play when you select/deselect the item in the scroller, of course.
  3. If you would like icons to go along with the scroller such as a description for each choice, set ShowIcon=true. Then it will load from "Graphics/ScreenSelectModeCustom icon choice" where ScreenSelectModeCustom is replaced by the name of your screen. Note that icons do not change position when you scroll.

scroller choice actor example

return Def.ActorFrame{
    LoadActor("Cool_Backing_Icon");
    --Text
    LoadFont("Common Normal")..{
        --[[
        Warning: This is not localization safe.
        If you want to translate it into other languages, use
        Text=THEME:GetString("ScreenTitleMenu",gc:GetText());
        instead and set the text in metrics to match
        ]]
        Text=gc:GetText();
        InitCommand=cmd(diffusealpha,0;shadowcolor,Color("Black");shadowlength,1);
        OnCommand=cmd(addx,-300;sleep,index/8;decelerate,.2;addx,200;);
        GainFocusCommand=cmd(stoptweening;linear,.1;addx,5;diffuse,Color("HoloBlue"));
        LoseFocusCommand=cmd(stoptweening;linear,.1;addx,-5;diffuse,Color("White"));
	};
}

Commands Glossary

All commands can also be used inside metrics if UseIconMetrics=true in metrics.

Command Name When it's activated
GainFocus Run when the icon gains focus
LoseFocus Run when the icon loses focus
SwitchToPageX Run when pages are switched (For MAX~EXTREME style themes)
LostSelectedLoseFocus ???
Change ???
InitialSelection Only played when DOUBLE_PRESS_TO_SELECT is true. Maybe plays on first press?
OffFocused OffUnfocused Plays when the screen activates its 'off' state.

Metrics Glossary

Metrics values type What they do
WrapCursor bool Allows you the cursor to loop back to the first item after reaching the end.
ScrollerSecondsPerItem float The time it takes for your scroller to move from one selection to another. If set to 0, the scroller will not move.
ScrollerNumItemsToDraw int Number of items to draw. duh.
ScrollerTransform Transform Function The scroller transform function.
ScrollerSubdivisions float Normally the scroller will only calculate the next point for the actor to scroll towards in the TransformFunction. Setting this to a value above 1 will add a point for it to tween towards before stopping at the end point. (In English: you can make circular strollers with it.)
ShowIcon bool If the scroller should load additional actors from Graphics/ScreenName Icon Choice (For example, ScreenSelectPlayMode icon Choice). Unlike scroll choice, icons do not change position when you scroll. Also read the description for PerChoiceIconElement.
ShowCursor bool If the scroller should load additional actors from Graphics/ScreenName CursorPX (For example, ScreenSelectDifficulty CursorP1 and ScreenSelectDifficulty CursorP2). This seems to be for the difficulty select in MAX~EXTREME style themes.
ShowScroller bool If the scroller should load actors from Graphics/ScreenName Scroll Choice (For example, ScreenSelectPlayMode Scroll Choice). Setting this to false would defeat the purpose of your scroller... Also read the description for PerChoiceIconElement.
WrapScroller bool Changes the appearance of the WrapCursor metric if it is set to true. What this option does specifically is unknown, but having WrapScroller and LoopScroller on will make the cursor loop forever instead of jumping back to the start with WrapCursor enabled.
LoopScroller bool Changes the appearance of the WrapCursor metric if it is set to true. What this option does specifically is unknown, but having WrapScroller and LoopScroller on will make the cursor loop forever instead of jumping back to the start with WrapCursor enabled.
ScrollerX ScrollerY float Take a wild guess.
PerChoiceIconElement bool If this is true, instead of loading 'Graphics/ScreenName Icon Choice' for every choice, it will instead load 'Graphics/ScreenName Icon ChoiceNAME_OF_CHOICE', where NAME_OF_CHOICE is the name of the choice defined in ChoiceNames of your metrics. I really recommend keeping this false since it's unnecessary and makes your theme very messy.
PerChoiceScrollElement bool Same as above except for 'Graphics/ScreenName Scroll Choice'. I really recommend keeping this false since it's unnecessary and makes your theme very messy.
SharedSelection bool If there are one or two cursors. I think ShowCursor has to be true for this to work.
ChoiceNames string A string containing the choices you want in your scroller. For example: "Game,Freestyle,Customize"
ChoiceXXXX gamecommand Where XXXX is a choice you defined with ChoiceNames. The command set here will be executed when pressing start. For example, ChoiceFreestyle="applydefaultoptions;text,FREE STYLE;setenv,GameMode,freestyle;screen,ScreenSelectPlayMode"
IconChoicePosFunction function Unknown purpose. Seems to be a function that returns a table of positions when given the number of choices. Perhaps used to position differently depending on number of elements in the scroller (Like IIDX' mode select screens?)

There are other metrics values available but it's unlikely you'll need them unless you're an advanced themer or trying to do something from the MAX~EXTREME era.

Old metrics you probably don't need type what they do
DoSwitchAnyways bool ???
AllowRepeatingInput bool ???
OverrideSleepAfterTweenOffSeconds bool Unknown purpose, false by default.
SleepAfterTweenOffSeconds float Unknown purpose. 0 by default.
NumCodes int Unknown Purpose
OptionOrderUp, OptionOrderDown, OptionOrderLeft, OptionOrderRight, OptionOrderAuto ??? ???
PreSwitchPageSeconds float Controls the animation for multi page scrollers (DDR EXTREME style)
PostSwitchPageSeconds float Controls the animation for multi page scrollers (DDR EXTREME style)
NumChoicesOnPage1 int This is for the old DDRMAX~EXTREME style difficulty select where there were two pages.
UseIconMetrics bool Just... Don't ever touch this. Don't use it. It's insane.
CursorPXOffsetXFromIcon CursorPXOffsetYFromIcon float Where PX is P1, P2... You need to define it for all the players.
DisabledColor color I have no idea what this does.
DoublePressToSelect bool Take a wild guess.
IconChoiceXSwitchToPage1Command IconChoiceXSwitchToPage2Command IconChoiceXOnCommand IconChoiceXOffCommand IconChoiceXOffFocusedCommand IconChoiceXOffUnfocusedCommand Command (You know, like cmd()) Where X is the icon index. The same as using commands in a lua scroller.
IdleCommentSeconds float For the announcer.
IdleTimeoutSeconds float ???
UpdateOnMessage MessageCommand? Possibly subscribes to a broadcast?

Caveats when using ScreenSelectMaster scrollers

Scroller icons draw below scroller choices

There is a fix for this but it's not easy. Disable icons in metrics. Then move all your icons to the overlay of your screen in BGAnimations, for example ScreenSelectStyle overlay.

Then inside your default.lua (ex. BGAnimations/ScreenSelectStyle overlay/default.lua) do:

local t = Def.ActorFrame{}
--This should be declared outside of an ActorFrame, InitCommand, OnCommand, etc. Right below the 'local t' declaration is a good place to put it.
local ChoiceNames = split(",",THEME:GetMetric(Var("LoadingScreen"),"ChoiceNames"))

--Insert this for loop wherever you want it to display, if you want it to display on top of everything then put this at the end.
for i,text in ipairs(ChoiceNames) do
    t[#t+1] = LoadActor("Choice_"..text)..{ --Ex. Choice_Easy, Choice_Medium, Choice_Hard
        InitCommand=cmd(Center);
        MenuSelectionChangedMessageCommand=function(self)
            if SCREENMAN:GetTopScreen():GetSelectionIndex(GAMESTATE:GetMasterPlayerNumber())+1 == i then
                --Do whatever you would do in GainFocusCommand here
                self:visible(true)
            else
                --Do whatever you would do in LoseFocusCommand here
                self:visible(false)
            end;
        end;
    };
end;

return t;

can't execute lua

Add an OffCommand to your screen's overlay layer:

t[#t+1] = Def.ActorFrame{
  OffCommand=function(self)
    --This assumes you already have the variable 'local ChoiceNames', if you don't then copy paste it from the above example
    local choice = ChoiceNames[SCREENMAN:GetTopScreen():GetSelectionIndex(GAMESTATE:GetMasterPlayerNumber())+1]
    if choice == "Easy" then --Replace Easy with the choice you defined in metrics, of course.
      --Do something here...
    elseif choice == "Arcade" then
      --Do something here...
    end
  end
}

ActorScroller

Using ActorScroller instead of ScreenSelectMaster provides some benefits (along with some drawbacks).

Pros:

  • Can have as many scrollers in one screen as you want. For example you can show a popup actorscroller asking if you want to play the tutorial when they select a choice.
  • Can jump more than one value at a time.
  • Can execute lua. In fact, you can do whatever you want when start, up, down, etc is pressed since the input handler is unrelated to the ActorScroller.

Cons:

  • Must implement your own input handler (Don't worry, the example tells you how)
  • Must reimplement GainFocus and LoseFocus (Don't worry, the example tells you how)
  • For tweens to work, actors must be wrapped in an ActorFrame.
  • Scroller items don't know their own index. Can be somewhat dealt with by assigning them an index in a for loop.

There is a Basic example in the Docs folder of StepMania to get you started.

Additionally all the constructors for an ActorScroller are documented in the wiki.

I won't explain the constructors or the example since you should read it yourself, but I'll provide an example with input handling and GainFocus/LoseFocus taken care of.

Step 1: Add input handling in metrics

This example will use the 'simple' input handling of CodeMessageCommand. Paste the CodeNames section in your custom screen and edit it accordingly. For this example the custom screen is called "ScreenSelectChapter".

[ScreenSelectChapter]
#Your standard stuff that every screen has. Since this is a custom screen, Fallback="ScreenWithMenuElements".
Fallback="ScreenWithMenuElements"
PrevScreen="ScreenTitleMenu"
NextScreen="ScreenSelectDifficulty"

#This is the part you want to copy and paste.
CodeNames="Left,Right,Down,Up,Select,Start,Back"
CodeLeft="Left"
CodeRight="Right"
CodeDown="Down"
CodeUp="Up"
CodeSelect="Select"
CodeStart="Start"
CodeBack="Back"

Step 2: Adding the scroller

Now declare your scroller somewhere in a screen layer. This example uses "ScreenSelectChapter overlay".

--The thing you want to be displayed in the ActorScroller. It has to be a table because I said so.
local scrollerItemTable = {"Item 1", "Item 2", "Item 3", "Item 4"}

--Declare your ActorScroller.
local as = Def.ActorScroller{
    --Like I said, the example and the wiki tells you everything you need to know about these constructors.
    NumItemsToDraw=0, --DON'T LEAVE THIS AT 0!!!! You can use NumItemsToDraw=(#scrollerItemTable)*2 if you want.
    SecondsPerItem=.3, --If you change this to 0, the scroller will not move.
    TransformFunction=function(self,offset,itemIndex,numItems)
        self:y(80*offset);
    end,

    -- Scroller commands. This is an example InitCommand.
    InitCommand=cmd(rotationz,15;xy,325,SCREEN_CENTER_Y;SetFastCatchup,true),
    --This is the input handler for the scroller.
    CodeMessageCommand=function(self, param)
        if param.Name == "Up" or param.Name == "Left" then
            if self:GetDestinationItem() > 0 then
                self:SetDestinationItem(self:GetDestinationItem()-1);
                SOUND:PlayOnce(THEME:GetPathS("Common", "value"), true);
            end;
        elseif param.Name == "Down" or param.Name == "Right" then
            if self:GetDestinationItem() < self:GetNumItems()-1 then
                self:SetDestinationItem(self:GetDestinationItem()+1);
                SOUND:PlayOnce(THEME:GetPathS("Common", "value"), true);
            end;
        elseif param.Name == "Start" then
            SCREENMAN:GetTopScreen():StartTransitioningScreen("SM_GoToNextScreen");
        elseif param.Name == "Back" then
            SCREENMAN:GetTopScreen():StartTransitioningScreen("SM_GoToPrevScreen");
        end;

        --GainFocus and LoseFocus aren't part of ActorScroller, they're part of ScreenSelectMaster. So we'll reimplement them.
        for i=1,self:GetNumItems() do
            -- C++ is 0-indexed while lua is 1-indexed. So 0 is the minimum for an ActorScroller even though lua items start at 1.
            if self:GetDestinationItem() == i-1 then
                self:GetChild(i):playcommand("GainFocus")
            else
                self:GetChild(i):playcommand("LoseFocus")
            end;
        end;
    end
}

Now below your scroller declaration, use a for loop to add the children to the scroller:

for i,v in ipairs(scrollerItemTable) do
    --Need to wrap in an ActorFrame for GainFocus & LoseFocus to work properly.
    as[#as+1] = Def.ActorFrame{
        Name=i; --Do not forget this! You need to give the items an index for GainFocus and LoseFocus to work!
        LoadFont("Common Normal")..{
            Text=v;
            GainFocusCommand=cmd(stoptweening;linear,.5;diffuse,Color("Red"));
            LoseFocusCommand=cmd(stoptweening;linear,.5;diffuse,Color("White"));
        };
    };
end;

Finally, add the actorscroller back to the main ActorFrame...

t[#t+1] = as;