12th July 2009

Making a Menu

When modding Fallout 3, you'll probably find many situations in which a menu is a useful tool. They are a very explicit way of getting user input, and are often used for things like options. Menus are made up of two parts - the "front end", the part that the user sees, is controlled by forms called messages. The "back end", which controls the results of buttons and the overall menu structure, is controlled via script.

This tutorial is aimed at understanding the scripted "back end" of menus. The skeleton of a script that controls a menu has two main functions. The first is to get user input, and the second is to update the menu according to this input.

In order to get user input, the function GetButtonPressed is used. If a button has been clicked since it was last called, GetButtonPressed returns an integer value equal to the index of the button pressed, and if no button has been pressed it will return a value of -1.

To update the menu, the function ShowMessage is used. ShowMessage causes a message to be displayed on the screen, so it needs to be used to open the menu and to change the current message displayed.

While there are a few ways in which messages can be controlled, I prefer to use a method that uses a token so I'll be describing that method here. With this method, the menu is opened when the token is added to the player's inventory, and when the menu is closed the token is removed.

Small Single-Level Menus

Most menus are fairly simple, and require only one set of user input. Such menus consist of only one level, which usually means that they use only one message as well. Here is an example script that controls a simple single-level menu:

ScriptName SmallSingleLevelMenuScript

int iButton

Begin OnAdd player

	ShowMessage MenuMessage

End

Begin GameMode

	set iButton to GetButtonPressed

	if iButton == -1 ; No button has been pressed yet
		Return
	elseif iButton == 0 ; Option 0
		; Option 0
	elseif iButton == 1 ; Option 1
		; Option 1
	endif
	RemoveMe

End

In order to open this menu, the token with this script attached will be added to the player's inventory via AddItem. When the item is added the player's inventory, ShowMessage is called to cause the message to appear. Once the message is opened, the GameMode block will not be running because the game will be in MenuMode, but for the first frame it will. Because of this, if GetButtonPressed returns a value of -1 (meaning no buttons have been pressed yet) a Return statement is used to terminate the script for that frame.

Once the menu is open, neither of the blocks in the script will run. The game will remain in MenuMode until the player presses one of the buttons on the message, which causes it to close. Once this happens, the game will re-enter GameMode so the script's GameMode block will start running again, and this time GetButtonPressed will return a value other than -1. Depending on just what that value is, the script will execute different code relevant to the selected option. After doing this, the token will remove itself, because now that its menu has been closed it is no longer needed.

Large Single-Level Menus

The next most complex form of menu is still essentially a single level, but can consist of multiple messages. This situation arises when there are too many choices for a single message, which has a limit of 10 buttons. In order to deal with this, a "More" button is typically included at the bottom of the menu that causes a method with more options to be shown. Here is an example script that might be used if there are 12 different options to choose from:

ScriptName LargeSingleLevelMenuScript

int iButton
int iAlternateMessage

Begin OnAdd player

	ShowMessage FirstOptionsMessage

End

Begin GameMode

	set iButton to GetButtonPressed

	if iButton == -1 ; No button has been pressed yet
		Return
	elseif iAlternateMessage == 0 ; FirstOptionsMessage

		if iButton == 0 ; Option 1
			; Option 1
		elseif iButton == 1 ; Option 2
			; Option 2
		elseif iButton == 2 ; Option 3
			; Option 3
		elseif iButton == 3 ; Option 4
			; Option 4
		elseif iButton == 4 ; Option 5
			; Option 5
		elseif iButton == 5 ; Option 6
			; Option 6
		elseif iButton == 6 ; Option 7
			; Option 7
		elseif iButton == 7 ; Option 8
			; Option 8
		elseif iButton == 8 ; Option 9
			; Option 9
		elseif iButton == 9 ; More
			ShowMessage SecondOptionsMessage
			set iAlternateMessage to 1
			Return
		endif
		RemoveMe

	elseif iAlternateMessage == 1 ; SecondOptionsMessage

		if iButton == 0 ; Option 10
			; Option 10
		elseif iButton == 1 ; Option 11
			; Option 11
		elseif iButton == 2 ; Option 12
			; Option 12
		elseif iButton == 3 ; More
			ShowMessage FirstOptionsMessage
			set iAlternateMessage to 0
			Return
		endif
		RemoveMe

	endif

End

It might look quite a bit longer than the previous script, but really there's only a few differences between this script and the last one. The most important distinction here is the new variable - iAlternateMessage. This variable stores which message is currently being displayed, and is used in such a way that the script can alter its result depending on which message is currently open.

You'll notice that the conditional statements that check the value of iButton are very similar to those in the previous script. They begin with checking if no button has been pressed, so that the token won't be removed before the first message is shown, and are then split up depending on which message is currently showing. Once the current message has been determined, the value of iButton is checked as normal.

Two important button results are the results of the buttons labeled "More" in both message. Clicking one of these buttons results in the other message being displayed. When one of these buttons is pressed, ShowMessage is called to show the appropriate message, then iAlternateMessage is set to the appropriate value so the script knows which message is being displayed, then a Return statement is used to terminate the script to prevent the token from being removed.

Small Multi-Level Menus

As you can see, as a menu becomes more complex, it is necessary for the script to store information on the current message so that correct button results can be executed. The next step in complexity is to have multiple levels in your menu, essentially creating a tree-like structure. In order to support this type of navigation, it'll become necessary to have one variable to store information on the current level the player is at, and a separate variable for each level to store the index of the button pressed. With information contained within these variables, it is possible to store navigation in a linear tree menu. It also becomes necessary to allow the player to move back and forth throughout the menu, which requires button results like the ones seen to move between menus in the previous example.

Here is an example of a small multi-level menu script, which allows the player to select one of two sub-menus, followed by a set of options to select:

ScriptName SmallMultiLevelMenuScript

int iButton0
int iButton1
int iMenuLevel

Begin OnAdd player

	ShowMessage Level0Message

End

Begin GameMode

	if iMenuLevel == 0 ; Level 0

		set iButton0 to GetButtonPressed
		if iButton0 == -1 ; No button has been pressed yet
			Return
		elseif iButton0 == 0 ; FirstLevel1Message
			ShowMessage FirstLevel1Message
		elseif iButton0 == 1 ; SecondLevel1Message
			ShowMessage SecondLevel1Message
		endif
		set iMenuLevel to 1

	elseif iMenuLevel == 1 ; Level 1

		set iButton1 to GetButtonPressed
		if iButton1 == -1 ; No button has been pressed yet
			Return

		elseif iButton0 == 0 ; FirstLevel1Message

			if iButton1 == 0 ; Option 0-0
				; Option 0-0
			elseif iButton1 == 1 ; Option 0-1
				; Option 0-1
			elseif iButton1 == 2 ; Back
				ShowMessage Level0Message
				set iMenuLevel to 0
				Return
			endif
			RemoveMe

		elseif iButton1 == 1 ; SecondLevel1Message

			if iButton1 == 0 ; Option 1-0
				; Option 1-0
			elseif iButton1 == 1 ; Option 1-1
				; Option 1-1
			elseif iButton == 2 ; Back
				ShowMessage Level0Message
				set iMenuLevel to 0
				Return
			endif
			RemoveMe

		endif

	endif

End

As you can see, the navigation information is stored via the iMenuLevel variable and the different button variables. First, the value of iMenuLevel is checked so that the script knows what level is currently being displayed. This information will tell the script what button values it needs to check in order to know which message is currently being displayed. At level 0, only the initial message will be shown, so no button values need to be checked. At level 1, however, the message that is currently being displayed depends on which button was pressed in the previous menu, so the value of iButton0 needs to be pressed. As the number of levels increases, the number of button variables must be checked also increases.

Once the message that is currently being displayed has been determined, the code is again essentially the same as in the script for a single-level menu. The value of the relevant button variable is checked, and the relevant result code is run. Like with the large single-menu script, two important types of result are the results that take the user forward in the menu structure, and those that take them back. Like with the large single-level menu, these results consist of changing the value of the variable that stores the user's current position in the menu, iMenuLevel in this case, and using ShowMessage to show the new message.

At the end of these special button results, a Return statement is used to exclude the "common result" for that message, which in this script consists of the the RemoveMe function for both level 1 messages, and increments the value of iMenuLevel in the level 0 message. Using RemoveMe will remove the token from the player's inventory, terminating the script, whereas incrementing iMenuLevel tells the script that the user is advancing to the next level in the menu. Another useful common result is to show the current message again, which will allow users to pick multiple options. This type of common result is particularly useful for options menus.

Large Multi-Level Menus

Like with single-level menus, messages in multi-level menus will sometimes require more buttons than are available, and will therefore require extra messages in order to accomodate the extra options. When using this layout, one or more extra variables are required in order to completely store the user's current position in the menu. How many extra variables you require depends on how you want the menu's "Back" buttons to work. If you want "Back" buttons to lead always to the primary message, even if the current message was selected from a secondary message, then you'll only need to use one extra variable to store navigation data.

If, instead, you want the "Back" buttons to lead to the exact message that the current message was selected from, then you'll need to have an extra variable for every menu level that has alternate messages. That way, when returning to a level, you'll be able to detect which exact message the user has navigated through to get to their current message. One particular consideration that you need to keep in mind with this method is that, when moving forward in the menu, you'll need to reset the variable that stores the alternate message so that the primary message is reached first. If you don't do this, then it may be possible to access a secondary message when a primary message should be reached instead.

Here is an example of a simple large multi-level menu which can only be exited via a "Done" button on the main message:

ScriptName LargeMultiLevelMenuScript

int iButton0
int iButton1
int iMenuLevel
int iAlternateMessage

Begin OnAdd player

	ShowMessage Level0Message

End

Begin GameMode

	if iMenuLevel == 0 ; Level 0

		set iButton0 to GetButtonPressed
		if iButton0 == -1 ; No button has been pressed yet
			Return
		elseif iButton0 == 0 ; PrimaryFirstLevel1Message
			ShowMessage PrimaryFirstLevel1Message
			set iAlternateMessage to 0 ; PrimaryFirstLevel1Message
		elseif iButton0 == 1 ; SecondLevel1Message
			ShowMessage SecondLevel1Message
		elseif iButton0 == 2 ; Done
			RemoveMe
		endif
		set iMenuLevel to 1

	elseif iMenuLevel == 1 ; Level 1

		set iButton1 to GetButtonPressed
		if iButton1 == -1 ; No button has been pressed yet
			Return

		elseif iButton0 == 0 ; FirstLevel1Message

			if iAlternateMessage == 0 ; PrimaryFirstLevel1Message

				if iButton1 == 0 ; Option 0-0
					; Option 0-0
				elseif iButton1 == 1 ; Option 0-1
					; Option 0-1
				elseif iButton1 == 2 ; Option 0-2
					; Option 0-2
				elseif iButton1 == 3 ; Option 0-3
					; Option 0-3
				elseif iButton1 == 4 ; Option 0-4
					; Option 0-4
				elseif iButton1 == 5 ; Option 0-5
					; Option 0-5
				elseif iButton1 == 6 ; Option 0-6
					; Option 0-6
				elseif iButton1 == 7 ; Option 0-7
					; Option 0-7
				elseif iButton1 == 8 ; More
					ShowMessage SecondaryFirstLevel1Message
					set iAlternateMessage to 1
					Return
				elseif iButton1 == 9 ; Back
					ShowMessage Level0Message
					set iMenuLevel to 0
					Return
				endif
				ShowMessage PrimaryFirstLevel1Message

			elseif iAlternateMessage == 1 ; SecondaryFirstLevel1Message

				if iButton1 == 0 ; Option 0-8
					; Option 0-8
				elseif iButton1 == 1 ; Option 0-9
					; Option 0-9
				elseif iButton1 == 2 ; More
					ShowMessage PrimaryFirstLevel1Message
					set iAlternateMessage to 0
					Return
				elseif iButton1 == 3 ; Back
					ShowMessage Level0Message
					set iMenuLevel to 0
					Return
				endif
				ShowMessage SecondaryFirstLevel1Message

			endif

		elseif iButton1 == 1 ; SecondLevel1Message

			if iButton1 == 0 ; Option 1-0
				; Option 1-0
			elseif iButton1 == 1 ; Option 1-1
				; Option 1-1
			elseif iButton1 == 2 ; Back
				ShowMessage Level0Message
				set iMenuLevel to 0
				Return
			endif
			ShowMessage SecondLevel1Message

		endif

	endif

End

As you can see, this script is basically a combination of the concepts introduced in the large single-level menu and in the small multi-level menu. Probably the most difficult concept to grasp when scripting complex menus like this is that of a "common result". I find that, when trying to decide on the common result of a particular message, it is best and easiest to think of the expected result for most of the buttons.

For example, in the first message of the last example, most of the buttons would be expected to advance the menu to the next level. Because of this, the common result for that menu sets the value of iMenuLevel to 1 - the value designated to the next level. For the other messages, the expected result is that the current message will be displayed again, so the common result involves showing the current message again via ShowMessage.

If a particular button result should exclude the common result, it should normally be ended with a Return statement. The only exception to this is if a button should result in the menu being closed, in which case RemoveMe should be called instead.