BdR

Level select or stage selection screen, like in Angry Brids

Recommended Posts

EDIT: I've put my code for a level select screen here, feel free to use
EDIT2: example now also uses separate states for levelselect and game, and stores progress to local storage.

https://github.com/BdR76/phaserlevelselect

 

I'm working on a Phaser game and at the moment I'm looking to add a level select screen. It's going to be a 5x5 grid of icons, the icons will represent if a level is unlocked or not yet completed, and it should display stars to how well the player has finished a previous level.

 

This is kind of a defacto standard in mobile games nowadays, see screenshot of Cut The Rope below what I mean:

 

 

CTR_LevelSelect-200x300.png

 

I'm using game.add.button and adding them as a grid of 5x5 buttons putting them into an array. But my question is, how can I add text 1,2,3..25 to each button?  And how can I procedurally add the 0..3 stars icons to a button? Or is it better to just use sprites instead of buttons? Any tips would be greatly appreciated. :)

Share this post


Link to post
Share on other sites

The groups suggestion is the best option here I think. Create a function which generates and returns a single button, with its background image, a number (this can be a Text or BitmapText object) and three star images (which can have different frames to represent lit or unlit). Then just use your for loop to call this function however many times you need, placing the returned group at the appropriate position for each iteration.

Share this post


Link to post
Share on other sites

Thanks for the tips :) I've got a working example now, see

https://github.com/BdR76/phaserlevelselect

 

I've got a follow up question though... The locked ions shake when you touch them to indicate it is locked. This is done using chained tween animations. However, if you repeatedly tap a locked icon it can drift out of position to the left or right. I know this is because it starts with the current x position and then changes it. But is there a way to check if an object still has a current tween animation? Or is there maybe another way to do a shake animation? See code below.

// shake it babyvar tween = game.add.tween(IconGroup).to({ x: xpos+4 }, 20, Phaser.Easing.Linear.None) .to({ x: xpos-4 }, 30, Phaser.Easing.Linear.None) .to({ x: xpos+4 }, 40, Phaser.Easing.Linear.None) .to({ x: xpos }, 50, Phaser.Easing.Linear.None) .start(); 
 

Share this post


Link to post
Share on other sites

I' ve dealt with this sort of thing by putting an isTweening property on the object, setting it to true and then using the onComplete callback of the tween to set it to false and checking it is false before setting a new tween off I'd like to see if there's a better way though. I bet there is!

And thanks for putting up your example on GitHub. That's been really useful for me to see.

Share this post


Link to post
Share on other sites

you could just set the x and y coordinates everytime before you start the tween.. easy fix :)

 
Okay, I've just added this approach, also updated the github example. At the beginning I keep the original x/y position values in the icon object, in separate new variables. And then at the start of a shake tween I just take those original x/y values.
// create new groupvar IconGroup = game.add.group();IconGroup.x = xpos;IconGroup.y = ypos;// also keep original position, for restoring after certain tweensIconGroup.xOrg = xpos;IconGroup.yOrg = ypos;

Btw coming from different programming language (mainly Delphi/pascal) I'm still getting used to the fact that in JS you can just add any random variable to an instance, even after it's created. :D

Share this post


Link to post
Share on other sites

Thank you for posting the sample code -- super useful for my understanding as I create a level selector screen in my game.  I do have a couple of probably dumb questions -- 

  1. How do you tie the Level to the appropriate action -- ie when user clicks Level 1 how does it know where to go (and would you recommend separate .js files for each level)
  • I assume this would be placed in function update() but a sample would help
  1. I assume it would be straight forward to remove the stars and just use the code to pass the level info  
  2. IS there a "best way" to pass the level data to the menu screen

Thanks in advance for any help/guidance

Share this post


Link to post
Share on other sites
Glad to hear you've found my example useful. :) First of all I've found it easiest to use the Phaser.States. I typically create a state for the main menu, the level select, and the maingame. Here is a good example of phaser states. And I wouldn't recommend separate .js files or states for each level, see my answer in this other thread.

 

And the best way to pass the selected level index to the MainGame state is I think to just set a levelindex variable in the MainGame state and handle the initialising of the level in MainGame state, based on that levelindex. For an example of passing variables from one state to the other see this thread. Using this LevelSelect example code could go like so:



// LevelSelect screen as a Phaser.State
mygame.LevelSelect.prototype = {
    create: function(){
        // .. initialise code goes here
    },

    // ..more code

    onLevelIconDown: function(sprite, pointer) {

        // retrieve the iconlevel, stored in sprite.health here
        var levelnr = sprite.health;
        console.log('onLevelIconDown - levelnr=' + levelnr);

        // simulate button press animation to indicate selection
        var IconGroup = this._icons[levelnr - this._offset - 1];
        var tween = this.game.add.tween(IconGroup.scale)
            .to({ x: 0.9, y: 0.9}, 100, Phaser.Easing.Linear.None)
            .to({ x: 1.0, y: 1.0}, 100, Phaser.Easing.Linear.None)
            .start();
            
        // start the level after tween is complete
        tween.onComplete.add(function(){this.onLevelSelected(sprite.health);}, this);
    },

    onLevelSelected: function(levelnr) {
        console.log('onLevelSelected ---> levelnr=' + levelnr);
        
        // pass levelnr variable to 'Game' state
        this.game.state.states['MainGame']._levelIndex = levelnr-1;
        this.game.state.start('MainGame');
        // etc.
    }
};

Share this post


Link to post
Share on other sites

Thanks for the information.  Let me say, when I take your code and copy it directly -- works like a charm (not surprising)  So my next trick was just to try and drop the code into my game and I have been unsuccesful.

 

I am using the following State to show a dashboard and high score of the game.  It works fine:

var states = {    main: "main",    game: "game",};var graphicAssets = {    menu:{URL:'assets/images/menuscreen-version3.png', name:'menu'},};
// MAIN STATEvar mainState = function(game){    this.tf_start;};mainState.prototype = {init: function(score) {    var score = score || 0;    this.highestScore = this.highestScore || 0;    this.highestScore = Math.max(score, this.highestScore);   },    preload: function () {                game.load.image(graphicAssets.menu.name, graphicAssets.menu.URL);        },    create: function () {            this.add.sprite(0, 0, 'menu');            //highest score    text = "HIGHEST SCORE: "+this.highestScore;    style = { font: "15px Arial", fill: "#fff", align: "center" };    var h = this.game.add.text(this.game.width/2, this.game.height/2 + 50, text, style);    h.anchor.x = 0.5;    h.anchor.y = -6.5;                game.input.onDown.addOnce(this.startGame, this);    },        startGame: function () {        game.state.start(states.game);    },};var game = new Phaser.Game(gameProperties.screenWidth, gameProperties.screenHeight, Phaser.AUTO, 'gameDiv'); game.state.add(states.main, mainState); game.state.add(states.game, gameState); game.state.start(states.main);

So I tried to apply the same general structure:

var states = {    main: "main",    menu: "menu",    game: "game",};var graphicAssets = {       menu:{URL:'assets/images/menuscreen-version3.png', name:'menu'},   levelselecticons:{URL:'assets/images/levelselecticons.png', name:'levelselecticons', width:96, height:96},    };var fontAssets = {    font72:{URL:['assets/fonts/font72.png', 'assets/fonts/font72.xml'], name:'font72'},};var PLAYER_DATA = [0,3,2,3,1,2];var holdicons = [];
// MENU STATEvar menuState = function(game){    };menuState.prototype = {    preload: function () {            game.load.spritesheet(graphicAssets.levelselecticons.name, graphicAssets.levelselecticons.URL, graphicAssets.levelselecticons.width, graphicAssets.levelselecticons.height, graphicAssets.levelselecticons.frames);        //game.load.bitmapFont(fontAssets.font72.name, fontAssets.font72.URL);        },    create: function () {        game.stage.backgroundColor = 0x80a0ff;game.add.text(256, 24, 'Select a level!', {font: '48px Arial', fill: '#FFFFFF', align: 'center'});    //game.add.bitmapText(256, 24, 'font72', 'Select a level!', 48);this.createLevelIcons();this.animateLevelIcons();    },        update: function () {// nothing to do but wait until player selects a level    },    render: function () {// display some debug info..?    },        // -------------------------------------    // Add level icon buttons    // -------------------------------------        createLevelIcons: function () {var levelnr = 0;for (var y=0; y < 3; y++) {for (var x=0; x < 4; x++) {// player progress info for this levellevelnr = levelnr + 1;var playdata = PLAYER_DATA[levelnr-1];// decide which iconvar isLocked = true; // lockedvar stars = 0; // no starsif (playdata > -1) {isLocked = false; // unlockedif (playdata < 4) {stars = playdata;}; // 0..3 stars};// calculate position on screenvar xpos = 160 + (x*128);var ypos = 120 + (y*128);// create iconholdicons[levelnr-1] = this.createLevelIcon(xpos, ypos, levelnr, isLocked, stars);var backicon = holdicons[levelnr-1].getAt(0);// keep level nr, used in onclick methodbackicon.health = levelnr;// input handlerbackicon.inputEnabled = true;backicon.events.onInputDown.add(this.onSpriteDown, this);   };   };    },    createLevelIcon: function (xpos, ypos, levelnr, isLocked, stars) {// create new groupvar IconGroup = game.add.group();IconGroup.x = xpos;IconGroup.y = ypos;// keep original position, for restoring after certain tweensIconGroup.xOrg = xpos;IconGroup.yOrg = ypos;// determine background framevar frame = 0;if (isLocked == false) {frame = 1};// add backgroundvar icon1 = game.add.sprite(0, 0, 'levelselecticons', frame);IconGroup.add(icon1);// add stars, if neededif (isLocked == false) {var txt = game.add.bitmapText(24, 16, 'font72', ''+levelnr, 48);var icon2 = game.add.sprite(0, 0, 'levelselecticons', (2+stars));IconGroup.add(txt);IconGroup.add(icon2);};return IconGroup;    },    onSpriteDown: function (sprite, pointer) {// retrieve the iconlevelvar levelnr = sprite.health;if (PLAYER_DATA[levelnr-1] < 0) {// indicate it's locked by shaking left/rightvar IconGroup = holdicons[levelnr-1];var xpos = IconGroup.xOrg;var tween = game.add.tween(IconGroup).to({ x: xpos+4 }, 20, Phaser.Easing.Linear.None).to({ x: xpos-4 }, 30, Phaser.Easing.Linear.None).to({ x: xpos+4 }, 40, Phaser.Easing.Linear.None).to({ x: xpos }, 50, Phaser.Easing.Linear.None).start();} else {// simulate button press animation to indicate selectionvar IconGroup = holdicons[levelnr-1];var tween = game.add.tween(IconGroup.scale).to({ x: 0.9, y: 0.9}, 100, Phaser.Easing.Linear.None).to({ x: 1.0, y: 1.0}, 100, Phaser.Easing.Linear.None).start();tween._lastChild.onComplete.add(function(){console.log('OK level selected! -> ' +sprite.health);}, this)//   };    },    animateLevelIcons: function () {// slide all icons into screenfor (var i=0; i < holdicons.length; i++) {// get variablesvar IconGroup = holdicons[i];IconGroup.y = IconGroup.y + 600;var y = IconGroup.y;// tween animationgame.add.tween(IconGroup).to( {y: y-600}, 500, Phaser.Easing.Back.Out, true, (i*40));   };    },};// GENERALvar game = new Phaser.Game(gameProperties.screenWidth, gameProperties.screenHeight, Phaser.AUTO, 'gameDiv');game.state.add(states.main, mainState);game.state.add(states.menu, menuState);game.state.add(states.game, gameState);game.state.start(states.menu);

Unfortunately, all I get is the colored background with a blank square.  I was able to add the title text but not using the bitmapped font.  I am glad I took this step before I tried to tack the passing of level data on figuring out the click to function.

 

I am hoping you can spot my mistake -- 

 

Thanks for any assitance.

Share this post


Link to post
Share on other sites


var states = {
    main: "main",
    menu: "menu",
    game: "game",
};


It might be that last comma after "game" but have you tried looking at the JavaScript console? In Chrome you can bring it up with Ctrl + shift + J. That usually displays pretty clear error messages marked in red, including the line number where it went wrong.

Share this post


Link to post
Share on other sites

THANK YOU.  Great suggestion and a tool I did not know about, but will definitely add to the tool box.  The issue was the font.  Although I had commented it out in one section -- 

//game.add.bitmapText(256, 24, 'font72', 'Select a level!', 48);

I missed some area and so it was causing the JS to hang. 

font72:{URL:['assets/fonts/font72.png', 'assets/fonts/font72.xml'], name:'font72'},// add stars, if neededif (isLocked == false) {var txt = game.add.bitmapText(24, 16, 'font72', ''+levelnr, 48);var icon2 = game.add.sprite(0, 0, 'levelselecticons', (2+stars));IconGroup.add(txt);

Not sure why this is causing an issue, but will troubleshoot this evening.

 

I will also be attempting to get the levels information to work and the user input.  I will let you know of my progress and hope you don't mind me asking for assistance if I get stuck.  

Share this post


Link to post
Share on other sites

I was able to overcome the font issue.  At one point, I got confused because there is no .xml for the big_font.png. But now the menu comes up as expected. In working with the fonts, I came across this thread which has online editors that some may fine useful - I know they have been to me tonight.

 

Regarding the level icons and clicking to go to the game -- before passing level data -- all I was attempting to do tonight is to drop in your code and see if I could click a level icon and have it take me to the game state.  Unfortunately, I have not been successful (and trust me it is not from lack of trying {or shall we say banging my head against keyboard}.  The dropped code worked fine (ie no errors), but I think where I am faulting is the place I need to call the these function.  I tried every possible location thinking the most appropriate 

 

var PLAYER_DATA = [0,3,2,3,1,1,1,2];var holdicons = [];var LevelData = [    {        title:"An easy level to start",        layout:"550f000202020000000002020000000202000000000202020001",        timelimit:120,        backcolor:"#3333ff",        mustscore:1000,        tutorialmessage:1    },    {        title:"Introducing enemies",        layout:"550f000000000040000200000200400000400002000002004000",        timelimit:75,        backcolor:"#33ffcc",        mustscore:1600,        tutorialmessage:2    },    {        title:"Now try this",        layout:"550f0000200000021116200024c0180010292202000010000001",        timelimit:100,        backcolor:"#9933ff",        mustscore:2500    },];var menuState = function(game){        var levelparams = LevelData[this._levelindex] };    onLevelIconDown: function(sprite, pointer) {        // retrieve the iconlevel, stored in sprite.health here        var levelnr = sprite.health;        console.log('onLevelIconDown - levelnr=' + levelnr);        // simulate button press animation to indicate selection        var IconGroup = this._icons[levelnr - this._offset - 1];        var tween = this.game.add.tween(IconGroup.scale)            .to({ x: 0.9, y: 0.9}, 100, Phaser.Easing.Linear.None)            .to({ x: 1.0, y: 1.0}, 100, Phaser.Easing.Linear.None)            .start();                    // start the level after tween is complete        tween.onComplete.add(function(){this.onLevelSelected(sprite.health);}, this);    },    onLevelSelected: function(levelnr) {        console.log('onLevelSelected ---> levelnr=' + levelnr);                // pass levelnr variable to 'Game' state        this.game.state.states['states.game']._levelIndex = levelnr-1;        this.game.state.start('states.game');        // etc.    }, 

My thought was that even though the data would be bogus that clicking would take me to the game state.

 

I am hoping you can help.  

Share this post


Link to post
Share on other sites

I've just updated my github example :) I've added two separate states for level select and game. When you select a level it starts the game state, and just for testing it immediately awards a random nr of stars. The nr of stars are stored in the progress array, and also the progress is saved to the local storage.

https://github.com/BdR76/phaserlevelselect

Share this post


Link to post
Share on other sites

Hi guys just making my first game and I manage to incorporate most of your code in this example, unfortunately I'm obligated to use different states so how do I approach adding 2 or more states like in this section here? Is my idea below correct? then I guess I declare the this._levelNumber = 0; as 1 and 2 and so on on each state? I'm a bit confused any help? :) also let me note that Level1 is on a separate .js all together but it works fine.

 


    onLevelSelected: function(levelnr) {

        console.log('onLevelSelected ---> levelnr=' + levelnr);

        

        // pass levelnr variable to 'Game' state

        this.game.state.states['Level1']._levelIndex = levelnr-1;

        this.game.state.start('Level1');

        this.game.state.states['Level2']._levelIndex = levelnr-1;

        this.game.state.start('Level2');

        // etc.

    }

};

 

Share this post


Link to post
Share on other sites
1 hour ago, Rafaelx said:

unfortunately I'm obligated to use different states so how do I approach adding 2 or more states like in this section here? Is my idea below correct?

I don't think your code example is correct, because it will .start() all the level states but you only want the selected level to start. If you have a Phaser.State for each level then you could do it something like this:

onLevelSelected: function(levelnr) {

    console.log('onLevelSelected ---> levelnr=' + levelnr);

    // determine which state to start
    var statename = 'Level' + levelnr;
    
    // start state 'Level1' or 'Level2' etc.
    this.game.state.start(statename);
}

Although I'm not really sure why you would want to have a separate Phaser.State for each level.. Maybe check out this thread about how you could pass different level data to a single gamestate.

Share this post


Link to post
Share on other sites

thanks I will try to use your approach to send data to same state, but mainly the problem is that some states have completely different set of rules not just time difference, also have different sprites, and things.

Share this post


Link to post
Share on other sites

thanks so If I want to pass parameters will this works?

 

	onLevelSelected: function(levelnr) {

    // determine which state to start
    var statename = 'Level' + levelnr;
    


		// pass levelnr variable to 'Game' state
		this.game.state.states[statename]._levelNumber = levelnr;
		
		this.state.start(statename);
	},

 

Share this post


Link to post
Share on other sites
2 hours ago, Rafaelx said:

thanks so If I want to pass parameters will this works?

Well have you tried it? ;) Yes I think that will work, as long as the state has the same variable names as you are setting in the levelselect state in onLevelSelected(). So something like this for example will work.

// Level3 state definition
Level3 = function(game){
    // define variables for Level3
    this._timeLimit = 1000;
    this._myColor = '#FFFFFF';
    this._startAnimation = true;
};

//..

// And then in your onLevelSelected()
this.game.state.states['Level3']._timeLimit = 500;
this.game.state.states['Level3']._myColor = '#4CAF50';
this.game.state.states['Level3']._startAnimation = false;

Although I don't really understand why you need to pass parameters at the start of each level, because all the rules are already programmed in each Level-state. Isn't it just a matter of starting the correct state name?

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.