Create a menu for Arduboy


(Kevin Pericart) #1

Hi Guys , i am new into arduboy coding and i have a project related to it. I have to do that :

  • when we launch the arduboy it have to display a message like “hello” for 6 seconds.
  • then it have to clear the monitor and display a menu with 3 options ( and we can choose between them ).
    so can you help me with this program ( i know C language but C++ is new for me ) and give me the solution for this with some comment in order to understand how exactly work the program.
    Thanks you :slight_smile:

(Boti Kis) #2

Hi Kevin,

This is partly a coding style thing. I usually tend to handle states and display the desired “scene” (in this case the menu) in the main loop when the scene is selected.

The following code is a function which creates its own loop and handles the menu and drawing all by itself. This can helpful in some cases but may have negative on resources (no details here).

When the function gets called, it won’t return unless the player takes a decision in the menu. By doing that it returns a state I have defined corresponding to a menu item pressed.

This is just one way to do it and probably not the best way (of there is one), but it should be understandable how a menu can work.

You can put this code directly in a .ino (or .cpp) file and call the function, or put it inside a class to call it as a method of the class. If you are new to c++ you should definitely take a look into classes and object oriented programming. You probably will be overwhelmed by all the new things so take it step by step. Arduboy is a great platform to learn all that.

// This is an enum class, similar to enums from C but with certain enhancements
enum class GameState:uint8_t{
  Menu = 0,
  SinglePlayer,
  MultiPlayer,
  Options
};

GameState showMenu(){

// this holds the index of the currently selected menu item
uint8_t cursorIdx = 0;

// Defines how many items the menu has
constexpr uint8_t menuItems = 3;

// These are helpers for layouting
constexpr uint8_t menuPosX = 10;
constexpr uint8_t menuPosY = 4;
constexpr uint8_t menuPadding = 6;

  // Game loop
  while(true){

    if (!arduboy.nextFrame()) continue;

    // Handle button presses
    arduboy.pollButtons();
    if (arduboy.justPressed(DOWN_BUTTON)){
      cursorIdx++;
    }
    if (arduboy.justPressed(UP_BUTTON)){
      if (cursorIdx == 0)
        cursorIdx = menuItems-1; // wrap around if below 0
      else
        cursorIdx--;
    }
    if (arduboy.justPressed(B_BUTTON)){

      // Single player
      if (cursorIdx == 0)
        return GameState::SinglePlayer;
      if (cursorIdx == 1)
        return GameState::MultiPlayer;
      if (cursorIdx == 2)
        return GameState::Options;

    }
    // Wrap the index around
    cursorIdx = cursorIdx%3;

    // Clear screen every frame
    arduboy.clear();

    // draw menu
    arduboy.setCursor(menuPosX, menuPosY + menuPadding*0);
    arduboy.print(F("ONE PLAYER"));
    arduboy.setCursor(menuPosX, menuPosY + menuPadding*1);
    arduboy.print(F("TWO PLAYER"));
    arduboy.setCursor(menuPosX, menuPosY + menuPadding*2);
    arduboy.print(F("OPTIONS"));

    // draw cursor
    arduboy.setCursor(2, menuPosY + menuPadding * cursorIdx);
    arduboy.print(F("<"));

    arduboy.display();
  }
}

(Scott R) #3

It’s a little dated now but you could potentially generate a menu with tiny choice https://www.tinychoice.net/ if you wanted to see another approach.


(Pharap) #4

Although this is probably the simplest way, I have a few issues with it:

  • Really you should avoid calling arduboy.display, arduboy.pollButtons etc outside of the main loop, if you start going into lots of mini game loops then it can become hard to keep track of which path execution is taking
  • It’s cheaper to pre-empt a change to a cursor than to try to correct it after the fact, especially with % (division is expensive)
  • It doesn’t feature the ‘hello for 6 seconds’
  • The text of the menu overlaps (because the default font is 7 pixels high, not 6, and you need an extra pixel for space)

So here’s a version that addresses those issues:

#include <Arduboy2.h>

// This is an enum class, similar to enums from C but with improved 'type safety'
enum class GameState : uint8_t
{
	Hello,
	Menu,
	SinglePlayer,
	MultiPlayer,
	Options
};

Arduboy2 arduboy;
GameState gameState = GameState::Hello;

void changeGameState(GameState newGameState)
{
	gameState = newGameState;
}

void setup()
{
	arduboy.begin();
}

void loop()
{
	if (!arduboy.nextFrame())
		return;

	arduboy.pollButtons();
	
	update();
	
	arduboy.clear();
	
	draw();
	
	arduboy.display();
}

void update()
{
	switch(gameState)
	{
		case GameState::Hello:
			updateHello();
			break;
		case GameState::Menu:
			updateMenu();
			break;
		case GameState::SinglePlayer:
			//updateSinglePlayer();
			break;
		case GameState::MultiPlayer:
			//updateMultiPlayer();
			break;
		case GameState::Options:
			//updateOptions();
			break;
	}
}

void draw()
{
	switch(gameState)
	{
		case GameState::Menu:
			drawMenu();
			break;
		case GameState::Hello:
			drawHello();
			break;
		case GameState::SinglePlayer:
			//drawSinglePlayer();
			break;
		case GameState::MultiPlayer:
			//drawMultiPlayer();
			break;
		case GameState::Options:
			//drawOptions();
			break;
	}
}

unsigned long helloTimer = 0;

void updateHello()
{
	constexpr unsigned long millisecondsPerSecond = 1000;
	constexpr unsigned long sixSeconds = (6 * millisecondsPerSecond);

	if(helloTimer == 0)
	{
		helloTimer = millis();
	}
	else
	{
		unsigned long now = millis();
		if((now - helloTimer) >= sixSeconds)
		{
			helloTimer = 0;
			changeGameState(GameState::Menu);
		}
	}
}

void drawHello()
{
	arduboy.print(F("Hello world!"));
}

// The index of the currently selected menu item
uint8_t cursorIndex = 0;

// How many items the menu has
constexpr uint8_t menuItems = 3;
constexpr uint8_t firstMenuItem = 0;
constexpr uint8_t lastMenuItem = (menuItems - 1);

void updateMenu()
{
	if (arduboy.justPressed(DOWN_BUTTON))
	{
		if(cursorIndex < lastMenuItem)
			++cursorIndex;
		else
			cursorIndex = firstMenuItem;
	}

	if (arduboy.justPressed(UP_BUTTON))
	{
		if(cursorIndex > firstMenuItem)
			--cursorIndex;
		else
			cursorIndex = lastMenuItem;
	}

	if (arduboy.justPressed(B_BUTTON))
	{
		// Single player
		switch(cursorIndex)
		{
			case 0:
				changeGameState(GameState::SinglePlayer);
				break;
			case 1:
				changeGameState(GameState::MultiPlayer);
				break;
			case 2:
				changeGameState(GameState::Options);
				break;
		}
	}
}

void drawMenu()
{
	// Layout helper constants
	constexpr uint8_t selectorPositionX = 2;
	constexpr uint8_t menuPositionX = (selectorPositionX + 8);
	constexpr uint8_t menuPositionY = 4;
	constexpr uint8_t menuPadding = 8;

	// Draw menu
	arduboy.setCursor(menuPositionX, menuPositionY + (menuPadding * 0));
	arduboy.print(F("ONE PLAYER"));
	
	arduboy.setCursor(menuPositionX, menuPositionY + (menuPadding * 1));
	arduboy.print(F("TWO PLAYER"));
	
	arduboy.setCursor(menuPositionX, menuPositionY + (menuPadding * 2));
	arduboy.print(F("OPTIONS"));

	// Draw cursor
	arduboy.setCursor(selectorPositionX, menuPositionY + (menuPadding * cursorIndex));
	arduboy.print('>');
}

This might look more complicated, but it uses less memory.

There’s a much better way to handle the menu too (using an array of strings and a for loop),
but that means explaining the __FlashStringHelper trick and the array size trick.