Impact

This forum is read only and just serves as an archive. If you have any questions, please post them on github.com/phoboslab/impact

1 decade ago by RomainT

Hello,

In the game I work on, I want to display a main menu with some items (Start, Help and Credits).
I want to display these menu items with just text and a bullet then I can select an item by hitting my up and down arrow. The bullet must appear in front of the selected item", then if I hit the space, a method is called on the item.

Any idea ? How can I implement this kind of menu ?

Thanks ! :)

1 decade ago by Hareesun

It’s not the most automatic of systems but you can write a function that basically does…

“If ‘up’ is pressed and the current selected item is item1 - return false”
“if ‘down’ is pressed and the current selected item is item1 - select item2”

“If ‘up’ is pressed and the current selected item is item2 - select item1”
“if ‘down’ is pressed and the current selected item is item2 - select item3”

“If ‘up’ is pressed and the current selected item is item3 - select item2”
“if ‘down’ is pressed and the current selected item is item3 - return false”

Or you can do away with all that mess and simple map each item to a specific key.

1 decade ago by RomainT

Here is my very first working-but-naive implementation (with CoffeeScript) and It lives in the draw method of my game class.

draw: ->
  @parent()  
  me = @
  
  @choices      = [ 'Start', 'Options', 'Help', 'Credits' ]
  @choice       ||= null
  @selectedItem ||= 0
  
  baseX = ig.system.width / 2
  baseY = 50
  
  text = "Startups from Hell"
  @font.draw(text, baseX, 12, ig.Font.ALIGN.CENTER)
  
  text = "Make a choice with UP and DOWN then hit SPACE to confirm."
  @font.draw(text, baseX, 160, ig.Font.ALIGN.CENTER)
  
  for choice, i in @choices
    y = baseY + i * 20
    @font.draw(choice, baseX, y, ig.Font.ALIGN.CENTER)
    
    realSelectedItem = Math.abs(@selectedItem) % @choices.length
    if realSelectedItem == i
      textWidth = @font.widthForString(choice)
      
      x = baseX - textWidth / 2 - @cursorWidth - 8
      @font.draw(">>", x, y - 1)
      
      x = baseX + textWidth / 2 + 8
      @font.draw("<<", x, y - 1)

  if ig.input.pressed('up')
    @selectedItem -= 1
  else if ig.input.pressed('down')
    @selectedItem += 1
  else if ig.input.pressed('interact')
    realSelectedItem = Math.abs(@selectedItem) % @choices.length
    if @choices[realSelectedItem] == 'Start'
      me.startGame()

I'm working on cleaner version with two classes : Menu and MenuElement.

1 decade ago by RomainT

Done.

I now have a module game.menu containing two classes : Menu and MenuElement.

ig
.module('game.menu')
.defines ->
  window.Menu = ig.Class.extend
    init: (@font, @choices)->
      @selectedChoice = 0
      
      @cursorLeft      = ">>"
      @cursorLeftWidth = @font.widthForString(@cursorLeft)
      
      @cursorRight      = "<<"
      @cursorRightWidth = @font.widthForString(@cursorRight)
      
      # Calculate the label length only once.
      for choice in @choices
        choice.labelWidth = @font.widthForString(choice.label)
        
    
    # Draw the menu begining at baseY in height.
    # The menu is centered around the baseX axis.
    draw: (baseX, baseY)->
      for choice, i in @choices
        y = baseY + i * 20
        @font.draw(choice.label, baseX, y, ig.Font.ALIGN.CENTER)
        
        if @selectedChoice == i
          x = baseX - choice.labelWidth / 2 - @cursorLeftWidth - 8
          @font.draw(@cursorLeft, x, y - 1)
          
          x = baseX + choice.labelWidth / 2 + 8
          @font.draw(@cursorRight, x, y - 1)
          
      if ig.input.pressed('up')
        @selectChoice(-1)
      else if ig.input.pressed('down')
        @selectChoice(+1)
      else if ig.input.pressed('interact')
        @choices[@selectedChoice].interact()
        
        
    # Change the menu choice in the given shift: 
    # +1 for bottom, -1 for the top.
    selectChoice: (shift)->
      index = @selectedChoice + shift
      
      if index < 0
        @selectedChoice = @choices.length - 1
      else
        @selectedChoice = index % @choices.length
        
  
  # A menu element that have a text label and a callback
  # triggered when the item hit.
  window.MenuItem = ig.Class.extend
    init: (@label, @action)->
    interact: ->
      @action()

My goal was to have as little computation as possible in the methods called in the game loop.

To use it, I initialize the menu in the init method of the game :

@menu = new Menu @font, [
  new MenuItem "Start",   @startGame
  new MenuItem "Options", @showOptions
  new MenuItem "Help",    @showHelp
  new MenuItem "Credits", @showCredits
]

Then I draw it in the draw method of the game :

@menu.draw(baseX, 50)

And it works !

1 decade ago by fugufish

wow it does look pretty in CoffeeScript! never thought of it that way.

1 decade ago by sleenee

Interesting , but what if you would want to write it in javascript? (I'm not familiar with coffeescript)
Did anyone here ever write a simple menu in javascript?

greetings,

Sleenee

1 decade ago by fugufish

what kind of menu? clickable or based on keyboard?

1 decade ago by sleenee

I would say clickable but I didn't know it makes that much of a difference. I guess there is nothing in the impact library in order to make building a choice menu / buy and sell menu / player dialog ... anything in that context a bit easier. (not that I saw at least)

So any hints on this?

thanks in advance,

Sleenee

1 decade ago by quidmonkey

Very nice. A pure javascript version would be sweet.

1 decade ago by Jack9

I did that this weekend. I translated the menu/item class code to javascript.

ig.module(
    'game.menu'
)
.defines(function(){

    window.Menu = ig.Class.extend({
        init: function(_font,_choices){
            this.selectedChoice = 0;
            this.cursorLeft = ">>";
            this.cursorRight = "<<";

            this.cursorLeftWidth = _font.widthForString(this.cursorLeft);
            this.cursorRightWidth = _font.widthForString(this.cursorRight);

            // Calculate the label length only once.
            var i,labeled_choice;
            for(i=0;i<_choices.length;i++){
                _choices[i].labelWidth = _font.widthForString(_choices[i].label);
            }
            this.font = _font;
            this.choices = _choices;
        },

        // Draw the menu beginning at baseY in height.
        // The menu is centered around the baseX axis.
        draw: function(_baseX, _baseY){
            var _choices = this.choices;
            var _font = this.font;
            var i,choice,x,y;
            for(i=0;i<_choices.length;i++){
                choice = _choices[i];
                y = _baseY + i * 12;

                _font.draw(choice.label, _baseX, y, ig.Font.ALIGN.CENTER);

                if (this.selectedChoice === i){
                    x = _baseX - (choice.labelWidth / 2) - this.cursorLeftWidth - 8;
                    _font.draw(this.cursorLeft, x, y - 1);

                    x = _baseX + (choice.labelWidth / 2) + 8;
                    _font.draw(this.cursorRight, x, y - 1);
                }
            }

            if(ig.input.pressed('up')){
                this.selectedChoice--;
                this.selectedChoice = (this.selectedChoice < 0) ? 0 : this.selectedChoice;
            }else if(ig.input.pressed('down')){
                this.selectedChoice++;
                this.selectedChoice = (this.selectedChoice >= _choices.length) ? _choices.length-1 : this.selectedChoice;
            }else if(ig.input.pressed('interact')){
                _choices[this.selectedChoice].interact();
            }
        }

        // Change the menu choice in the given shift:
        // +1 for bottom, -1 for the top.
        ,selectChoice: function(shift){
            var index = this.selectedChoice + shift;

            if(index < 0){
                this.selectedChoice = this.choices.length - 1;
            }else{
                this.selectedChoice = index % this.choices.length;
            }
        }
    });

    // A menu element that have a text label and a callback
    // triggered when the item hit.
    window.MenuItem = ig.Class.extend({
        init: function(_label, _action){
            this.label = _label;
            this.action = _action;
        },
        interact: function(){
            this.action();
        }
    });
});

1 decade ago by Jack9

I wanted to implement a menu on an EntityCreature, so here's a simplified version of what's in my entity (there may be commas misplaced):

    init: function( x, y, settings ) {
        var i;
        var choices = [
            new MenuItem("Option1",this.OptionMethod1),
            new MenuItem("Option2",this.OptionMethod2),
            new MenuItem("Option3",this.OptionMethod3),
            new MenuItem("Option4",this.OptionMethod4)
        ];
        this.menufont = new ig.Font( 'media/04b03.font.png' );
        this.contextMenu = new Menu(
            this.menufont,
            choices
        );

        // Call the parent constructor
        this.parent( x, y, settings );
    }

    ,update: function() {
        // This method is called for every frame on each entity.
        // React to input, or compute the entity's AI here.
        // In your game's or entity's update() method
        if( ig.input.pressed('context') ) {
                if(this.checkSelection()){
                    this.context();
                }else{
                    this.decontext();
                }
        }

        // Call the parent update() method to move the entity
        // according to its physics
        this.parent();
    }

    ,checkSelection:function(){
        return (
            (ig.input.mouse.x >= this.pos.x && ig.input.mouse.x <= this.pos.x+this.animSheet.width)
         && (ig.input.mouse.y >= this.pos.y && ig.input.mouse.y <= this.pos.y+this.animSheet.height)
        );
    }

    ,decontext:function(){
        this.contexted = false;
    }
    ,context:function(){
        if(!this.contexted){
            this.contexted = true;
        }
    }

	,draw: function() {
		// Draw all entities and backgroundMaps

        if(this.contexted && this.contextMenu){
            this.contextMenu.draw(this.pos.x+(this.animSheet.width/2),this.pos.y+(this.animSheet.height));
        }
		this.parent();
	}

Finally, in my main draw I put:

	draw: function() {
		// Draw all entities and backgroundMaps
		this.parent();
        if( ig.input.pressed('interact')) {
            var i,creatures;
            creatures = this.getEntitiesByType("EntityCreature");
            // clear all other selections
            for(i=0;i<creatures.length;i++){
                if(creatures[i].contexted){
                    creatures[i].decontext();
                }
            }
        }
    }

That took a lot to figure out, but it works as I would expect.

1 decade ago by sleenee

@Jack9

This is a really brilliant solution, thank you for this!

I will try to experiment with your code as basis.

Sleenee

1 decade ago by sunnybubblegum

I just tried implementing Jack9's menu, but I can't seem to get it working. I'm not even entirely sure how or where it's supposed to appear. Was wondering if anyone could offer some simple instructions for setting this (or similar) up. Thank you!
Page 1 of 1
« first « previous next › last »