Space Engineers

Space Engineers

Not enough ratings
EasyMenu
By rockyjvec
This guide is a tutorial on how to use the EasyMenu script.
   
Award
Favorite
Favorited
Unfavorite
The Goal
Our goal when we are done with this tutorial is to have a working menu that is displayed on an LCD. We will control the menu from our cockpit. The menu will be able to play a sound from a sound block, control four doors, and show the current door status (open/closed) in a sub-menu.

Here is what we will end up with:
Getting Started
**************** READ THIS!!!! ************************
The latest version of EasyAPI is slightly different than the version used in this script. So the constructor in the Example class will look slightly different than what it shows in the screenshots.

It should now look like this:
public class Example : EasyAPI { public Example(IMyGridTerminalSystem grid, IMyProgrammableBlock me, Action<string> echo, TimeSpan elapsedTime) : base(grid, me, echo, elapsedTime) { // Start your code here } }

Sorry for any inconvenience that may cause. Comment below if you need help.

I'm working on a new version of this tutorial that uses a better method of user input that's made possible by the latest version of Space Engineers.
***********************************************************


We will be using the EasyAPI script to make the example menu.

I highly recommend that you use Notepad++[notepad-plus-plus.org], a free text editor, to edit the code. It highlights the code so it is easier to read. The screenshots of the code in this tutorial are from Notepad++. Make sure you save your text file with the .cs file extension so Notepad++ knows that the file is C#.

The first thing we need to do, since we are going to be using EasyAPI, is to copy and paste the EasyAPI code into a new text file (or the programming block if you are not using Notepad++).

Click here for the EasyAPI code[raw.githubusercontent.com]

Next, we need to copy and paste the EasyMenu code at the end of our text file (or programming block)

Click here for the EasyMenu code[raw.githubusercontent.com]

By the way, I should mention that all of my EasyAPI related scripts are on GitHub. Feel free to fork, contribute new features, and submit pull requests. I'm new to C# so I'm sure many improvements can be made :-).

Yay!, Now we're ready to get started. You should now have the following empty EasyAPI example class at the top of your text file (or programming block).


By the way, you can get the source code we will be building in this tutorial here[raw.githubusercontent.com]
Menu Basics
Okay, before we get too far into actually making a menu, we need to learn how the menu works.

Separation of Concerns

The EasyMenu class is designed so that it can be used in as many cases as possible. For this reason, it is not responsible for actually displaying the menu, and it is not responsible for getting input from a player.

Some people may want to display the menu on an LCD, others may want to build a giant array of lights to display the menu on. Absurd example, I know, but knowing gamers, it will probably happen, lol.

For input, some people will want to control the menu from the cockpit, and others may want to control it from a button panel.

For these reasons, the EasyMenu class simply renders the menu to a string and leaves it up to the user how to display it. It also just provides some basic functions to call to manipulate the menu rather than actually handling player input itself.

Basic Menu Functions

The menu class has four basic functions to manipulate the menu. They are:

Choose() = Choose the selected menu item. When an item is selected, it will run the code associated with the item, if any, and/or go to the sub-menu, if it exists.

Back() = Go up one level. If you in a sub-menu, it goes to the parent menu. If you are at the top most menu, then it does nothing.

Up() = Move the cursor up one line in the menu.

Down() = Move the cursor down one line in the menu.

Displaying the menu

As I mentioned above, the EasyMenu class is not responsible for actually displaying the menu. Instead, you can get a string containing the menu by calling the Draw() method and then use that string however you want to display it. You can pass in the width and height contraints in characters for the menu. If the menu is too tall to fit in those constraints it will automatically scroll. The defaults are good for a wide LCD screen that has it's font size set to 2.5.

For example, to display the menu on an LCD you might do something like this:
lcd.SetText(menu.Draw(70, 7));

Menu Appearance

The basic menu looks like this:
Main Menu » Sub-Menu > Item 1 < Item 2 Item 3

You can see the title text of the menu is displayed at the top in breadcrumbs format. So you can always see where you are in the menu. The next line is empty, then each item in the current menu is displayed. The selected item will be wrapped in '>' and '<' like Item 1 above. In the future I plan on having the menu text scroll right and left if it is too wide to fit in a given number of characters.

Block Setup
Okay. Let's get all the blocks setup.

For this tutorial, you will need the following blocks:
  • Four doors named "Door 1", "Door 2", "Door 3", and "Door 4"
  • A programming block which you will paste the code into.
  • A timer block named MenuTimer
  • A wide LCD named MenuLCD
  • A flight cockpit
  • A sound block named MenuSpeaker
It's best to place all the blocks so that they are all visible from the cockpit. You want to be able to see the menu on the LCD and the other blocks. Especially the doors.

Make sure they are all owned by you to prevent any permissions issues later on.

In the LCD, change it to display public text. You will probably need to adjust the font size later on to make the menu fit how you want it on the LCD.

Choose a sound in the sound block. Also, make sure that the range is set far enough so you can hear it from the cockpit.

In the timer block, set the actions so it runs the programming block and then triggers the timer block (itself). DON'T SET THE TIMER TO START ITSELF, MAKE SURE YOU USE TRIGGER.

Enter the cockpit. In the 'G' menu, map the following actions to action bar numbers 1-4

1. MenuTimer - Decrease Delay
2. MenuTiemr - Increase Delay
3. MenuLCD - Increase image change interval
4. MenuLCD - Decrease image change interval

Watch the example video again if you need to see how these are setup.

Later on we will make these actions trigger the menu actions.
Hook up the LCD
Before we get to the menu, we need to first get that LCD to display some text.

EasyAPI comes with an EasyLCD class which makes it easy to use an LCD. I'm not going to go too deep into the details of how EasyAPI works in this tutorial since you can visit my EasyAPI guide to learn more about that.

Okay, first things first. We need an instance of the EasyLCD class that has been initialized with our MenuLCD block. EasyAPI makes this trivially easy. Let's put the variable in the Example class scope (outside the constructor) so that we can use it in our various methods we will be implementing later.

Variables in an EasyAPI class keep their state between calls to the programming block. The constructor only runs the first time the programming block runs so we will initialize the LCD, menu, and other blocks in that method. It probably sounds more complicated than it is. :-)

Here is what your code should look like:

The FindOrFail("MenuLCD not found!") call is optional. It makes debugging easier because if will dump the "MenuLCD not found!" message to the exception area in the programming block if there is no block named "MenuLCD" on your ship.

As a quick test to make sure the LCD is working, let's output a message to it.

The SetText() method in the EasyLCD class is used to change the text in an LCD.

Just add the following line after line 9 in the code above, then compile, save and remember, and run the programming block (it actually should run automatically since you already set the timer up to run it constantly.). You should see the "Hello World!" text on the LCD.
this.lcd.SetText("Hello World!");

If it worked, you can remove that line.
User Input
This is probably the most complicated part of this tutorial. Unfortunately there is currently no easy way to read the state of buttons or keys using the Programming block. So to get around this limitation we have to use buttons/keys to trigger values to change, then, in the programming block, we can read the values to determine if a button/key was pressed. In this case, if a value increases, one button was pressed, and if a value decreases, a different button was pressed.

We will use two different values to read the input of the four actions we set up in the cockpit a little while ago.

We will be using the Timer Delay value from the MenuTimer block for the first two buttons, and the Image change interval value from the MenuLCD block for the second two buttons.

We can easily get those blocks using EasyAPI. Like the EasyLCD class, we want to make these blocks (EasyBlocks class) accessible from the Example class scope. We might as well get an instance of the sound block now as well.

Below is how we set them up. We can also refactor the EasyLCD line since it just takes an instance of the EasyBlocks class to initialize it and that way we don't need to search for the LCD block twice.



Okay. Now, to figure out how a button was pressed, we want to check if one of the values increased or decreased. Then, if it did change, we can set it back to a default value so that we can detect it again. I use "5" as the default value. We can easily get and set values using EasyAPI. And we can schedule a function to be run at a regular interval to check for button updates.

The Every(time, function) method in EasyAPI allows you to schedule a function to be run every *time* amount of time.

I don't want to go into a lot of detail about how EasyAPI works since there is another guide for that. Basically, here is how we check the button/key statuses:



That's it, we now have user input! You could test it out if you want to by writing something to the lcd in each section where it says "// button * pressed". Make sure you compile and save/remember the programming block, then, in the cockpit, press each of the action buttons 1-4. The LCD should display your text.

Note that if you put the timer and lcd increase/decrease actions we used for input into a button panel you can also use that as a controller for the menu
Our First Menu
Okay, finally, let's create our first menu!

To build a menu, we need to use two different classes:
  • EasyMenu - This is the base menu class. It is responsible for controlling and rendering the menu.
  • EasyMenuItem - This class is responsible for each item in the menu. It handles things such as rendering the item text and handling actions in the menu.

To create a menu, we simply create an instance of the EasyMenu class and pass in the name of the menu, followed by an array of all the menu items.

So here is the code to create a basic "Test Menu" that has only a single item that does nothing:



In order for the menu to show up, we need to render it and display the string on our LCD. To do that, let's add the following call to menu.Draw() to our doUpdate function. We will also add code to clear the LCD before drawing the menu so that we don't get strange text artifacts on our menu. We'll also save the menu object in the Example class scope so we can use it in all our methods.


Finally, let's put the menu actions, Choose(), Back(), Up(), and Down() into the doUpdates() function so our menu is fully functional.


Yipee! Now if you compile, save and remember, and run the script, you should have a fully functional menu. Although it doesn't do anything yet since there is only one option. :-)
Adding an Action
Let's add our first action to the menu.

We will add a new item to the menu, "Play Sound", which will play the Sound Block we added earlier when you choose that menu item.

There are four different EasyMenuItem constructors which make it easier to add items for different purposes. Basically they just reorder the parameters so you don't have to supply default values for things you aren't going to use.

The available parameters are:
  • text - The static text to display in the menu. The text that gets displayed can be overwritten by the textAction parameter below.
  • children - If you are making a sub-menu, you will supply the items in the sub-menu using this parameter. It is just another array of EasyMenuItem objects.
  • chooseAction - This is a callback function that will get run when someone chooses this menu item. The item object will get passed into the callback function so you can reference the item text if you need to. It should return a bool value. True if you want the sub-menu (if it exists) to load and false if you don't
  • textAction - If supplied, this callback function, which is also passed the menu item object, should return the text that should be displayed in the menu. We will use this later to display the door status.

The Standard Constructor - new EasyMenuItem(text, children, chooseAction, textAction)

The Action Constructor - new EasyMenuItem(text, chooseAction, textAction)

The Static text constructor - new EasyMenuItem(text)

The Dynamic text constructor - new EasyMenuItem(textAction, chooseAction)

To play a sound when the menu item is chosen, we'll use the Action Constructor.

First, let's create the function that will get called when the menu item is chosen. Using EasyAPI it is easy to play the sound block so it will look like this:


Now we just need to add the menu item to our menu like this:


Copy/paste, compile, save and remember, and try it out. You should be able to change between the two items in the menu, and if you press the choose button on "Play Sound" you should hear the sound block play.
Adding a Sub-Menu
Adding a sub-menu is easy. You simply treat a menu item as a new menu, passing in an array of menu items as the second parameter.

Let's add a "Door Status" sub-menu that has the names of the four doors in it:


Try it out. If you select the "Door Status" menu item, you will be taken to the sub-menu that lists the four doors. Press the "Back" button (probably 2 if you set everything up the same as the example) to get back to the main menu.

You can add as many sub-menus within sub-menus as you want.
Dynamically Changing Menu Text
The final thing we need to do to complete our example is to add actions to close and open the doors, and show the status of the doors in the menu.

Let's start with opening/closing the doors. Remember from the Sound Block example that the callback functions have the EasyMenuItem object passed in. This allows us to reuse the same function for multiple items in the menu, changing it based on the item text. So, we can write a function that opens/closes the door specified in the item text.

Here is that door open/close function:


Notice that we use item.Text to find a door with the name of the item in the menu.

Now we just need to add that action to the menu items like this:


Go ahead and give that a try. When you "choose" one of the doors, it should open or close.

Now, let's change it so the status of the door gets displayed in the menu.

The textAction parameter takes a function that gets passed the EasyMenuItem and returns a string. So what we want to do is, based on the item Text, look up the door with that name, and return a string that contains the door name, followed by its status (Open/Closed). We can do that like this:


Now we just need to add the doorStatus function to our menu so that function is used to display the menu text like this:


Give it a try. You should now how a fully functional menu that works exactly like the demo video.

Here is the full code in case you have any problems:

Bonus: Dynamically Generating Sub-Menus
This lesson goes beyond the scope of the example video and covers an advanced type of menu which I call a dynamic sub-menu.

So far we have only covered static menus where you define exactly what menu items you want and they never change. In this lesson we will be learning how to generate a sub-menu on the fly.

For the example in this lesson we will change our Door Status menu so that instead of toggling a door on/off when you choose a door, it instead generates a new sub-menu which lists all of the available actions for that door and lets you trigger each action.

Note: this lesson uses features that didn't work properly until 2015/05/02 so you may need to update the menu classes in your script for it to work if you did the tutorial before that date.

In past lessons you probably noticed that we are given access to the selected menu item in chooseAction functions. We have only been using the menu item so far just to get its text in order to determine which door to open. Well another thing we can do with the menu item is change its children. That is what we are going to be doing now.

The first thing we will be doing is getting a list of all of a block's available actions. We can easily do this in EasyAPI by simply using the GetActions() method on the block. We'll just add that to our toggleDoor function for now.

EasyBlock door = Blocks.Named(item.Text).FindOrFail(item.Text + " not found!").GetBlock(0); var actions = door.GetActions();

Next, we'll clear out any existing children from the menu item so we can generate a new list.

EasyBlock door = Blocks.Named(item.Text).FindOrFail(item.Text + " not found!").GetBlock(0); var actions = door.GetActions(); item.children.Clear();

We need to also remove the old door toggle code, and change the function from return false (no sub-menu) to true (go to sub-menu).

Finally, we'll loop through the actions and simply add a new menu item for each action with a chooseAction hard-coded to apply the action.

Here is how the function should look when it is done:

Here is the code for copy/paste:
public bool toggleDoor(EasyMenuItem item) { EasyBlock door = Blocks.Named(item.Text).FindOrFail(item.Text + " not found!").GetBlock(0); var actions = door.GetActions(); // Get all possible door block actions item.children.Clear(); // clear out any old menu items for(int n = 0; n < actions.Count; n++) { var action = actions[n]; // Add action to the menu item.children.Add(new EasyMenuItem("" + action.Name, delegate(EasyMenuItem actionItem) { door.ApplyAction(action.Id); // when the action is chosen in the menu, apply it. return false; // there is no sub-menu for actions })); } item.children.Sort(); // this will sort the actions by their text return true; // go to sub-menu }


For another, even more complicated example of dynamic menus, see my ShipExplorer script on GitHub[github.com].

My ship explorer script automatically generates a menu of every type of block on your ship, then in each of those menus, it generates a menu of every block of that type, then for each block, it generates a menu with every possible action for that block. So, basically, you can apply any action to any block on your ship from the menu.

Here is a demo video:
Closing Thoughts
I hope this guide was helpful. If you need any help, post a comment below or in the EasyMenu workshop page.
18 Comments
DasKrusty 10 Mar, 2019 @ 12:38am 
I honestly cant figure out if I'm supposed to start with the easy API code and then paste the easy menu code in there somewhere and then add code and Ive read through all the easy API and easy menu and it just doesn't make sense to me. You need to dumb this down for me and personally I think you bounce around too much in your guide. Why not supply the script in one instead of giving me multiple places to go to get things I don't know if I need... See I'm lost here and REALLY want to figure this out because I hate loads of things or scripts on my hot bar... Would REALLY dig having an LCD with this menu system going.
Emiricol4Rl 11 Jan, 2019 @ 1:38pm 
It occurs to me that, with this app, I can finally use VoiceAttack in Space Engineers. It'll be worth it to learn how to use this (I'm no coder) just to get that :)
rockyjvec  [author] 19 Jan, 2016 @ 3:54pm 
@Mazen IIXIS, yeah, I started to rewrite it using the On() function but haven't finished. In github I have example code for using that.

I usually use both Every (through timer) and On so that the menu gets updated even if you are not pressing a button. If you don't have status updates in the menu then just using On will work. It won't conflict.
Mazen IIXIS 10 Jan, 2016 @ 9:20pm 
@rockyjvec maybe you could update your tutorial to reflect the change in user input. I've decided to do an experiment and found out that you can use On("Up", Up) with public void Up() { menu.Up(); } and it works the exact same way as if you're taking in the lcd and timer properties. Using the On() function will be better. I will be using the On() functions from now on as long as the codes don't break again.
Mazen IIXIS 10 Jan, 2016 @ 3:25pm 
I have a question about user input. You said there's no easy way of getting inputs from buttons. That was before they put in the run by argument? What if I was to put in 4 programmable blocks into the buttons set to run with arguments "Up", "Down", "Choose", and "Back", will that work or because it is going to be running off the timer block that can cause conflict between them? What if the script is set to run both Every(1*Seconds, function) and On("Up", function) in the constructor? What will the result be?
rockyjvec  [author] 16 May, 2015 @ 10:24am 
@kortikal, I updated most of the scripts last night with a new feature and I just noticed I broke the example code I linked below. I just fixed it. Basically this needs to be added after EasyBlocks speaker:
public Example(IMyGridTerminalSystem grid, IMyProgrammableBlock me, Action<string> echo, TimeSpan elapsedTime) : base(grid, me, echo, elapsedTime)

Here is the up-to-date file that is fixed:
https://raw.githubusercontent.com/rockyjvec/EasyAPI/master/modules/EasyMenu/Examples/Example.cs?

Sorry about that.
kortikal 16 May, 2015 @ 8:56am 
Thx !
Zacknetic 15 May, 2015 @ 2:03pm 
@rockyjvec Thanks, now I can add and subtract variables :)
rockyjvec  [author] 15 May, 2015 @ 1:59pm 
@kortikal, the completed example code is available here: https://raw.githubusercontent.com/rockyjvec/EasyAPI/master/modules/EasyMenu/Examples/Example.cs

You will just need to copy and paste that, followed by the EasyAPI code and EasyMenu code which are linked in the Getting Started section above.
kortikal 15 May, 2015 @ 5:19am 
Hey, i'll actally try to ad it to my ship, i'm a rookie c#. It will be awesome if we could copie/past all cods you give in screenshoots, coz i'm rookie and i'll do a lot of mistake (it take me 15h to do it i think ^^). I know i'm busy, but it's hard for me and rookie people ! (awesome work)