Making a Menu

From Spherical
Jump to: navigation, search

Warning.svg

Warning: Legacy code ahead

This page has been written for the legacy Sphere 1.x API. While this API is supported in miniSphere, it is generally recommended to use the new Sphere 2 API.

You can mix old and new API functions, but please be aware that Sphere 2 objects are different from Sphere 1 objects, so they cannot be used interchangeably.

So, you've seen the menu.js in the sphere code bank, and you've used it but don't like it? What do you do? You make your own menu! You can choose to follow either the basic or the advanced route. The advanced route only makes it so that you can overflow your items without worry.

Menu.js

So, to go about making a menu you would need a general object to put properties in. This is the basics of OOP. Our menu object will look like this, which is very similar to Sphere menu.js:


function Menu()
{
  if (!this instanceof Menu) return new Menu();
  this.windowstyle = GetSystemWindowStyle();
  this.font = GetSystemFont();
  this.arrow = GetSystemArrow();
  this.items = [];
}

So, the above initializes the starting data that we will be using to create our menu.


this.items = [];

Is a shorthand for making an array. In JS anything can go into these arrays.


Adding Items

We can add our own items like so:

Menu.prototype.addItem = function(name, description, callback, color)
{
  if (color == undefined) color = CreateColor(255, 255, 255);
  var item = {
    name: name,
    desc: description,
    callback: callback,
    color: color
  };

  this.items.push(item);
}

The:

item = {};

Is shorthand for object creation. Notice that within these objects, you do not use the 'this' keyword, and also notice there are no more semicolons, instead there are commas indicating it's a lust of properties. This pseudo item will help us determine what is going to be in our menu.


Basic Drawing

Menu.prototype.execute = function(x, y, w, h)
{
  var font_height = this.font.getHeight();
  var selection = 0;

  while(!IsKeyPressed(KEY_ESCAPE))
  {
    this.windowstyle.drawWindow(x, y, w, h)
    for (var i = 0; i < this.items.length; ++i)
    {
      this.font.setColorMask(this.items[i].color);
      this.font.drawText(x+16, y + i * font_height, this.items[i].name);
    }

    this.arrow.blit(x, y +i * selection);

    FlipScreen();
  }
}

Three, the above code will draw the items you have added. It iterates through a for loop and draws all of the items names at an x position and a y position plus an offset so they do not all draw at the same location.


Advanced Drawing

So, what happens if you add more than one item? We will get an overflow of sorts and it'll draw items beyond the height of the menu we are using. To correct this we would need an offset from the top.

We will need to add overflow arrows to the menu:

function Menu()
{
  if (!this instanceof Menu) return new Menu();
  this.windowstyle = GetSystemWindowStyle();
  this.font = GetSystemFont();
  this.arrow = GetSystemArrow();
  this.upArrow = GetSystemUpArrow();
  this.downArrow = GetSystemDownArrow();
  this.items = [];
}

Now the drawing loop will add an offset value:

Menu.prototype.execute = function(x, y, w, h)
{
  var fontHeight = this.font.getHeight();
  var max_shown = Math.floor(h / font_height);
  var offset = 0;
  var selection = 0;

  while(!IsKeyPressed(KEY_ESCAPE))
  {
    this.windowstyle.drawWindow(x, y, w, h)
    for (var i = 0; i < max_shown; ++i)
    {
      if (i >= items.length) continue; // limit what is drawn
      this.font.setColorMask(this.items[i+offset].color);
      this.font.drawText(x+16, y + i * font_height, this.items[i+offset]);
    }

    this.arrow.blit(x, y + (selection-offset) * font_height);
    if (offset > 0)
      this.upArrow.blit(x + w - this.upArrow.width, y);
    if (offset + max_shown < this.items.length)
      this.downArrow.blit(x + w - this.downArrow.width, y + font_height * max_shown - this.downArrow.height);

    FlipScreen();
  }
}


Handling the selection (basic)

For this demonstration the handling of the menu will be added after the items are drawn. Pressing the up key shall move a cursor up, while pressing the down key shall move the cursor down. Hitting enter shall choose the item, and hitting escape shall quit the menu.

Menu.prototype.execute = function(x, y, w, h)
{
  // local variables //

  while(!IsKeyPressed(KEY_ESCAPE))
  {
    // stuff drawn here // 

    FlipScreen();

    while(AreKeysLeft())
    {
      switch(GetKey())
      {
        case KEY_UP:
          if (selection > 0) selection--;
        break;
        case KEY_DOWN:
          if (selection < this.items.length-1) selection++;
        break;
        case KEY_ENTER:
          this.items[selection].callback();
          return;
        break;
        case KEY_ESCAPE:
          return;
        break;
      }
    }
  }
}

It may look long, but this is basically finished. I commented out the stuff we already covered, so it should be obvious to add the while(AreKeysLeft()) loop. The reason why this loop works is because AreKeysLeft() is always going to be false until a key has been hit. Once the key is hit, it goes into the switch with GetKey(). GetKey() would grab that key and do whatever based on the switches.


Handling the selection (advanced)

So, the above doesn't take into account for the offset, so I'll teach you how to use handling with the more advanced menu. Start by writing the code from the previous section if it has not been written already. Now, let's focus on these specific cases:

        case KEY_UP:
          if (selection > 0)
          {
            selection--;
            if (selection < offset) offset--;
          }
        break;

And Down:

        case KEY_DOWN:
          if (selection < this.items.length-1)
          {
            selection++;
            if (selection >= offset + max_shown) offset++;
          }
        break;

That's all you would need to handle an advanced menu. all I did was take into consideration the new offset value.


Finished

Regardless if you went the basic way or the advanced way, here is how you may use it:

RequireScript("menu.js"); // notice that it's not a system script, it's yours.

function game()
{
  var my_menu = new Menu();
  my_menu.addItem("item 1", "my item", Exit);
  my_menu.addItem("item 2", "my item", Exit);
  my_menu.addItem("item 3", "my item", Exit);
  my_menu.addItem("item 4", "my item", Exit);
  my_menu.addItem("item 5", "my item", Exit);

  my_menu.execute(GetScreenWidth()/2-80, GetScreenHeight()/-40, 160, 80);
}


Extra: Drawing a background

I'll teach you how to use a custom render script:

function Menu()
{
  if (!this instanceof Menu) return new Menu();
  this.windowstyle = GetSystemWindowStyle();
  this.font = GetSystemFont();
  this.arrow = GetSystemArrow();
  this.items = [];

  this.preRender = function() {} // this is what you can add
}

And then in the drawing loop, no matter if you chose the advanced way or basic way:

Menu.prototype.execute = function(x, y, w, h)
{
  // local variables //

  while(!IsKeyPressed(KEY_ESCAPE))
  {
    this.preRender();
    // stuff drawn here // 

    FlipScreen();

    while(AreKeysLeft())
    {
      // handle choices //
    }
  }
}

Notice I commented the extra stuff out. Basically you add this.preRender() right after the very first while loop.

To use it:

RequireScript("menu.js");

function game()
{
  var my_menu = new Menu();
  my_menu.addItem("item 1", "my item", Exit);
  my_menu.addItem("item 2", "my item", Exit);
  my_menu.addItem("item 3", "my item", Exit);

  my_menu.preRender = function()
  {
    if (IsMapEngineRunning()) {
      RenderMap();
      UpdateMapEngine();
    }
  }

  my_menu.execute(GetScreenWidth()/2-80, GetScreenHeight()/-40, 160, 80);
}

The above will update and render the map behind the menu, given if the map engine is running or not. You can do this for all sorts of things, not just rendering.