Step by Step Guide on How to build your first Slider Puzzle game in Cocos2d for Android – Part 2

Screenshot_2014-02-07-02-51-04

In part 1 of the tutorial, we set up the game scene, added a couple of labels to keep track of game status , amount of time elapsed and number of moves. And we made the timer start counting to give a real game feel. Now we’ll build on that, add some tiles to the scene, and some game logic to the slider puzzle game . By the end of this tutorial, you’ll learn more about scaling sprites, positioning and handling touch response.

Generating Tiles

First, we will need to create a new method (under the scene() method) to generate the slider puzzle tiles and arrange them on the screen in a matrix manner. Technically, we can generate any amount of tiles we want e.g 3 X 3 = 9 tiles , 4 X 4 = 16 tiles  ,4 X 5 = 20 tiles etc. We would just need to adjust the height of the tile itself to fit the screen width and height.  The larger the number of tiles, the smaller our tile height will be. For this tutorial, we will use a  3 X 3 matrix of 9 tiles as shown in the picture above.
Before we proceed, we will need some global variables to be added at the top of our class The rational behind defining variables at the top of your class is to give them a global scope (i.e we can modify the variable from any method within our code). Some variables such status label which we will be updating from different methods will also be initialized at the top of our class for easy reference. An error will arise after you initialize this at the top, you may remove the second instance of initialization and simply use the variable.

Add the following variables to the top of your class.

private static final int TILE_NODE_TAG = 23;  //keeps track of each tile on the scene
private static   float TILE_SQUARE_SIZE = 3;  //the height of each of our tiles
private static final int NUM_ROWS = 3;    //Number of rows our game would support
private static final int NUM_COLUMNS = 3;  // Number of colums supported
private int toppoint = 0 ;     //top cordinate from which we would start laying out our tiles
private int topleft = 0;
CCBitmapFontAtlas statusLabel ;    //status label
private static CGPoint emptyPosition ;   //keeps track of the position of the empty slot on our game
float generalscalefactor = 0.0f ;     //a scaling factor to ensure our game looks good on diff screen sizes
private int moves = 0 ;                //number of moves
private Context appcontext;             //a reference to the android context variable
public static boolean gameover = false ;    //track if the game has been solved

Code snippet to generate tiles is given below, add the method to your class.

public void generateTiles(){

		//We create a Node element to hold all our tiles
		CCNode tilesNode = CCNode.node();
		tilesNode.setTag(TILE_NODE_TAG);
		addChild(tilesNode);
		float scalefactor ;   // a value we compute to help scale our tiles
		int useableheight  ;
		int tileIndex = 0 ;

		//We attempt to calculate the right size for the tiles given the screen size and
		//space left after adding the status label at the top
		int nextval ;

		int[] tileNumbers = {5,1,2,8,7,6,0,4,3};  //random but solvable sequence of numbers

		//TILE_SQUARE_SIZE = (int) ((screenSize.height  *generalscalefactor)/NUM_ROWS) ;
		int useablewidth = (int) (screenSize.width - statusLabel.getContentSize().width*generalscalefactor ) ;
		useableheight =  (int) (screenSize.height  - 40*generalscalefactor - statusLabel.getContentSize().height * 1.3f*generalscalefactor) ;

		TILE_SQUARE_SIZE = (int) Math.min((useableheight/NUM_ROWS) , (useablewidth/NUM_COLUMNS)) ;

		toppoint = (int) (useableheight  - (TILE_SQUARE_SIZE / 2) + 30*generalscalefactor)   ;
		scalefactor = TILE_SQUARE_SIZE / 150.0f ;

		topleft = (int) ((TILE_SQUARE_SIZE / 2) + 15*generalscalefactor) ;

		CCSprite tile = CCSprite.sprite("tile.png");
		//CCSprite tilebox = CCSprite.sprite("tilebox.png");

		for (int j = toppoint ; j > toppoint - (TILE_SQUARE_SIZE * NUM_ROWS); j-= TILE_SQUARE_SIZE){
			for (int i = topleft ; i < (topleft - 5*generalscalefactor) + (TILE_SQUARE_SIZE * NUM_COLUMNS); i+= TILE_SQUARE_SIZE){ 				if (tileIndex >= (NUM_ROWS * NUM_COLUMNS)) {
					break ;
				}
				nextval = tileNumbers[tileIndex ];
				CCNodeExt eachNode =  new  CCNodeExt();
				eachNode.setContentSize(tile.getContentSize());
				//
				//Layout Node based on calculated postion
				eachNode.setPosition(i, j);
				eachNode.setNodeText(nextval + "");

				//Add Tile number
				CCBitmapFontAtlas tileNumber = CCBitmapFontAtlas.bitmapFontAtlas ("00", "bionic.fnt");
				tileNumber.setScale(1.4f);

				eachNode.setScale(scalefactor);
				eachNode.addChild(tile,1,1);
				tileNumber.setString(nextval + "");
				eachNode.addChild(tileNumber,2 );

				if( nextval != 0){
					tilesNode.addChild(eachNode,1,nextval);
				}else {
					emptyPosition = CGPoint.ccp(i, j);
				}

				//Add each Node to a HashMap to note its location
				tileIndex++;
			}
		}

	}

The code above has been commented to give a clear idea of whats happening. To summarize whats being done, first we calculate the appropriate tile height (TILE_SQUARE_SIZE) to use given available screensize, calculate the appropriate starting point to lay out our tiles (toppoint) and then use two for loops to position of our tiles horizontally and vertically (x and y cordinates).

positioningtiles
Note about generating the tile numbers . We generate the number sequence our tiles like so

int[] tileNumbers = {5,1,2,8,7,6,0,4,3};

When we display the tiles,  0 represents the empty tile spot. Also, the randomness of the numbers generated should follow a certain order else the puzzle will not be solvable :). You can learn more about that here .

Finally, we use a custom Node Class CCNodeExt which has an attribute called nodetext. This helps us keep track of the node and we’ll use that later on. The CCNodeExt Class is given below, create the CCNOdeExt.java class and add it to your src folder in your project.

/**
	 *
	 * Author:  Victor Dibia
	 * Date last modified: Feb 10, 2012
	 * Model tiles with extra field NodeText
	 */

package com.example.puzzlegame;

import org.cocos2d.nodes.CCNode;

public class CCNodeExt extends CCNode{
	public  String nodeText ;

	public CCNodeExt(){
		super();

	}

	public void setNodeText(String nText){
		this.nodeText = nText;
	}

	public String getNodeText(){
		return this.nodeText ;
	}

}

Catching User Touch/Slide Action

In order to get touch response on our layer, we need to enable touch response reception on our layer. In Cocos2D,this is done by adding the following line in the layer default constructor ( GameLayer() in our case).

 this.setIsTouchEnabled(true); 

Add this inside the GameLayer() method.

Now, in order to slide our tiles, we employ a bit of Math. Remember, we laid out each tile using some mathematical calculation, thus we know the exact square cordinates that each tile falls within. Since we can now accept touch response, we can capture the exact position on the layer that the user has touched and also tell if it falls within any of our tile boxes. In Cocos2D, several method can be used to track user touch response. the ccTouchesBegan method is used to track when each touch event begins.
Paste the ccTouchesBegan snippet below as a method in your Gamelayer class

@Override
	public boolean ccTouchesBegan(MotionEvent event)
	{
		//Get touch location cordinates
		CGPoint location = CCDirector.sharedDirector().convertToGL(CGPoint.ccp(event.getX(), event.getY()));
		CGRect spritePos ;

		CCNode tilesNode = (CCNode) getChildByTag(TILE_NODE_TAG) ;
		//ccMacros.CCLOG("Began", "Began : " + location.x + " :  "  );

		//We loop through each of the tiles and get its cordinates
		for (int i = 1 ; i < (NUM_ROWS * NUM_COLUMNS); i++){
			CCNodeExt eachNode = (CCNodeExt) tilesNode.getChildByTag(i) ;

			//we construct a rectangle covering the current tiles cordinates
			spritePos = CGRect.make(
					eachNode.getPosition().x - (eachNode.getContentSize().width*generalscalefactor/2.0f),
					eachNode.getPosition().y - (eachNode.getContentSize().height*generalscalefactor/2.0f),
					eachNode.getContentSize().width*generalscalefactor   ,
					eachNode.getContentSize().height*generalscalefactor   );
			//Check if the user's touch falls inside the current tiles cordinates
			if(spritePos.contains(location.x, location.y)){
				//ccMacros.CCLOG("Began Touched Node", "Began touched : " + eachNode.getNodeText());
				slideCallback(eachNode); // if yes, we pass the tile for sliding.

			}
		}

		return true ;
	}

If we successfully get a hit, we call the slideCallback method below which determines which direction the touched tile should slide to. A tile that has no space next to it will not move at all .

See Also : How to Create a Sliding Menu in Cocos2d for Android

Basically the function is passed a reference to the slide that has been touched. Then we have 4 if statements that checks if the empty spaced is at the right , left , top or bottom of the touched tile. We do this because we keep track of the position of the empty spot using the emptyPosition variable. We then pass this direction found to the SlideTile function (coming up next) which slides the tile and updates the emptyPosition variable! Cunning huh ? 🙂

 

	public void slideCallback(CCNodeExt thenode) {

		CGPoint nodePosition = thenode.getPosition();

		//Determine the position to slide the tile to .. ofcourse only if theres an empty space beside it

		if((nodePosition.x - TILE_SQUARE_SIZE)== emptyPosition.x && nodePosition.y == emptyPosition.y){
			slideTile("Left", thenode,true);
		}else if((nodePosition.x + TILE_SQUARE_SIZE) == emptyPosition.x && nodePosition.y == emptyPosition.y){
			slideTile("Right", thenode,true);
		}else if((nodePosition.x)== emptyPosition.x && nodePosition.y == (emptyPosition.y  + TILE_SQUARE_SIZE )){
			slideTile("Down", thenode,true);
		}else if((nodePosition.x )== emptyPosition.x && nodePosition.y == (emptyPosition.y  - TILE_SQUARE_SIZE)){
			slideTile("Up", thenode,true);
		}else{
			slideTile("Unmovable", thenode,false);
		}

	}

The actual sliding is done by the SlideTile method below

 

	public void slideTile(String direction, CCNodeExt thenode, boolean move){
		CCBitmapFontAtlas moveslabel = (CCBitmapFontAtlas) getChildByTag(MOVES_LABEL_TAG);

		if(move && !gameover){
			//  Increment the moves label and animate the tile
			moves ++ ;
			moveslabel.setString("Moves : " + CCFormatter.format("%03d", moves ));

			//Update statuslabel
			statusLabel.setString("Tile : " + thenode.getNodeText() + " -> " + direction);

			//Animate the tile to slide it
			CGPoint nodePosition = thenode.getPosition();
			CGPoint tempPosition = emptyPosition ;
			CCMoveTo movetile = CCMoveTo.action(0.4f, tempPosition);
			CCSequence movetileSeq = CCSequence.actions(movetile, CCCallFuncN.action(this, "handleWin"));
			thenode.runAction(movetileSeq);
			emptyPosition = nodePosition ;

			//Play a sound
			appcontext = CCDirector.sharedDirector().getActivity();
			SoundEngine.sharedEngine().playEffect(appcontext, R.raw.tileclick);
			thenode.runAction(movetileSeq);
		}else{
		}

	}

In the above code, we update the moveslabel with the number of moves label, update the status label with the tile details on the tile movement, we perform the slide animation and we play a sound. For the sound, you will need to create a folder inside your “res” folder and name it “raw”. Now place your mp3 sound file within the res/raw folder. You can download the tileclick.mp3 file here

.

At this point, your should have a slider puzzle that works really well and looks like this
Screenshot_2014-02-07-02-51-04

Note: Our puzzle cant tell yet when the player has correctly arranged the tiles. We need to write a method to check the state of the game each time a slide action is done. We have referenced this method “handleWin” in this line.

CCSequence movetileSeq = CCSequence.actions(movetile, CCCallFuncN.action(this, "handleWin"));

Handling the win situation will be done in the next tutorial.

Update! Part 3 of Tutorial Available – Adding A Win Condition for your game

Next Steps

There are a few things you can do to improve the puzzle game

  • Create more rows and columns in your puzzle ?
  • Enable automatic generation of tiles and write code to generate a sequence of solvable tiles. You can start with examining this code .
  • Convert this into a letter puzzle ?
  • Raise the bar and push this up into a picture puzzle ?
  • Implement a win situation by tracking when the user has correctly solved the puzzle and trigger a game-over callback.
  • Manage your audio better . Give users a change to enable or disable sound within the game . Play some background music to give context to you game experience. Link the sound volume levels to the hardware sound buttons on your android device.
  • Implement achievements, a leader-board and connect the app with gaming sdks  such as scooreloop ?

To get more inspiration, check out gidigames [now FULLY Open Sourced], which contains a similar puzzle with a few extra bells and whistles

Gimme that Code!

Following the tutorial step by step might have been a little daunting, with some errors popping up. You can download the full code (bug free) we have written through a github repo that can be found here . Good luck and Godspeed in your Cocos2D for android journey!

Update! Part 3 of Tutorial Available – Adding A Win Condition for your game

About Vykthur

Mobile and Web App Developer and Researcher. Passionate about learning, teaching, and recently - writing.
This entry was posted in Android Tutorials, Cocos2d, Cocos2D for android, Developer Tips, Education, Programming and tagged , , , , , , . Bookmark the permalink.
  • Lou!z

    Vykthur, a want to display a diferent image in everyone of the tiles on the activity, how can I do that?

    • Vykthur

      The simple (naive) way to do this would be to create a different sprite image for each tile. I imagine you would like to upgrade your code into an image puzzle. There are two ways.
      1.) You break your image into different tile images … put them in a string array and assign them as the background tile image instead of tile.png we used in this tutorial.
      2.) You use a single image and assign only a portion of this image (based on calculations) to as the background image for your sprite. This is the recommended way to go about it but takes more effort. It can be done using the a 2D texture – CCTexture2D .
      You can learn more about the 2D texture class here …
      http://www.cocos2d-x.org/reference/native-cpp/V2.2/d7/dfd/classcocos2d_1_1_c_c_texture2_d.html

      The reference in the link above is for objective C but should be helpful in getting you started.

    • Vykthur

      Update..

      I have written a tutorial on how to do that.. here
      http://denvycom.com/blog/step-by-step-slider-picture-puzzle-game-using-cocos2d-for-android/

      Cheers
      V.

  • Turk

    I want to ask you if you know any good examples(other than yours :d) about game devolopment for android in cocos2d..I mean test classes are really good for understanding the methods of classes..But i dont know many methods that can help me..For example i couldnt make a man move,or dance(moving an individual sprite is not a problem),,Using sprite methods i tried to use the .pngs to make animation..There are good examples for ios but android is lack of examples.Also there is no doubt i am lazy to understand all methods looking to cocos2d classes and I forget basic C++,java that make harder to understand.Well anyway,you can forget about everything i wrote :d just can you make an animation that use dancing grossini maybe or something else.It has the pictures in cocos2d assets but in test i didnt see that animation also if you know the name of test using that sprite it will help..

    • Vykthur

      HI Turk ..
      Basically, what you need is the full range of animation classes . Classes like CCMoveTo, CCRotateTo etc …
      In the test sets, theres a class named ClickandMoveTest.java, they contain the grossini animation you want.
      In this file, grossini is animated to whatever location on screen that you touch.
      I’ll do a little more tutorials on this soon .Will let you know.

      Regards,
      Victor

      • Turk

        Thanks for help..Actually i wanted to run
        private void startAnim() {
        centerSprites();

        //tamara.setVisible(false);

        CCAnimation animation = CCAnimation.animation(“dance”, 0.2f);
        for (int i = 1; i < 15; i++) {
        animation.addFrame(new CCFormatter().format("grossini_dance_%02d.png", i));
        }

        CCIntervalAction action = CCAnimate.action(animation);

        grossini.runAction(action);

        }
        this from SpriteTests class but i failed to make it work..

  • Jodhi Satrio

    Thank you for your tutotial
    but i want to make a new activity that can choose a difficulity level of easy, medium and hard
    that differ in row and column
    how can i do that?
    i tried to pass value to NUM_COLUMNS and NUM_ROWS, but it is no good either
    it said non-static method cant make a reference to a static
    while i already make all variable non-static

    • Vykthur

      Hi Joghi,
      I’m glad the tutorials have helped thus far.
      Fist, where do you set the value of numrow and numcolumn? I think this should be done at the top of the class where they are declared. Also you should increase the content of tile[] integer array to hold sat 20 numbers instead of 9 in the example..

      This should work..
      Want to paste a part of your code / error?

      V.

      • Jodhi Satrio

        Wow fast reply

        from a new activity that i call level i have 3 buttons
        one of them is typed like this
        public void onClick(View v){
        switch(v.getId()){

        case R.id.playEasy:
        Intent pe = new Intent(this, MainActivity.class);
        pe.putExtra(“NUM_ROWS_FROM_LEVEL”, 3);
        startActivity(pe);
        break;

        In GameLayer.class
        private static final int NUM_ROWS = getIntent().getIntExtra(“NUM_ROWS_FROM_LEVEL”);

        but i can make this word because static reference, even though i already tried delete all static word

        thanks for your very fast reply

        • Vykthur

          Hi Jodhi,

          Technically, you cannot use geIntent() in GameLayer.java class because this does not inherit from the Android Activity class. Rather its a interits from a lowly CClayer class in cocos2d.

          A better solution would be to set the value of numrows and numcolums as a public static variable in your MainActivity.java file … e.g
          public static int NUM_ROWS= 3 ;

          and then use it in your GameLayer class like this.

          int numrows = MainActivity.NUM_ROWS

          Getting to understand how the cocos2d class sits on top of the Android Activity is important so I have written a new tutorial to provide further understanding…

          http://denvycom.com/blog/anatomy-of-a-cocos2d-game/

          Hop this helps …

          -V

          • Jodhi Satrio

            Can you teach me how to use if button in cocos2d?
            i have searched it everywhere but still didnt find it, even in stack overflow

  • abbas

    i don’t know which buffer i am overflowing but the screen shot is attached..serious noob here..:D…

    help wanted;;;

    • Vykthur

      Hi Abbas,

      I imagine you may have added some code to MainActivity.java that brings about this error.
      Can you tell me if you are using the code I provided on Github as is ? If you have added modifications, I would need to see your code in order to help .

      Also the error screenshot gives a hint that the error is on the MainActivity java file .. and the line number.

      You can share your code on a google document (or github) and post the link here …

      Goodluck!
      v.

  • urmyfaith

    there are some suggestions for you.
    1.break this article into some small pieces.
    2. offer a source zip of this project (one article ,one zip) instead of just a finally project on github.
    3.yourr website is so solw to load
    4. some of your code in this article is warped in one line ,but they should be in separate line, eg in method :”ccTouchesBegan”

    thanks for your sharing.

    • Vykthur

      HI Urmyfaith,

      Many thanks for your detailed suggestions. They all are great!
      1.) I have broken down the article into 3 parts already … do you feel each one is still too long ? Will definitely work on that ..
      2.) I used to have a zip file for the first part of the project … then I got comments and I learned better ways of solving some issues .. Im still learning like you. Updating a single git project and the article is less prone to error (compile errors especially) and easier than maintaining several zips. More importantly, I think most people use the article/guide as a starting point/reference in addition to that which comes with cocos.
      3.) You are right, my website needs help definitely. I think the problem is mainly from two sources. A better hosting provider (buying a new hosting service) and a site redesign/optimization (gzipping assets, using spritesheets, minifying js and css, implementing caching etc) . Solving both need significant resources, and Im working on them 🙂 .
      4.) Thanks for pointing that out .. warped cctouches and slidercallback code have been fixed!

      -V

  • Jerry Hernandez

    Hi! just as I finished the tutorial part 2, I could not run the applications. And heres what Logcat shows. Any Help Here? and also a broken link to part 3.

    • Jerry Hernandez

      I realised the application failed after i added this in.

      • Vykthur

        Hi Jerry,

        First of, thanks for pointing out the broken link … Iv just fixed that .
        Second, about the crash the error says theres a null pointer on Line 13 of your Gamelayer.java. I suspect theres a variable that may not have been properly initialized.

        I have also published the full working version of this tutorials on Github. You can crosscheck your current code with this …
        https://github.com/chuvidi2003/PuzzleGame/blob/master/src/com/example/puzzlegame/GameLayer.java

        Hope this helps.

        V

        • Jerry Hernandez

          Hi Vykthur,

          I’ve have look into it. No matter what I do, Logcat have been pinpointing on “import org.cocos2d.types.CGRect;” . Not sure what I did. I tried taking it away and re-importing it, but no luck.

          • Jerry Hernandez

            I copied the whole code(the link you shared) into mine. and I get exactly the same problem. So I ended up download the PuzzleGame-Master, and I attempted to import into my eclipse but to no avail. I guess that some files were missing. such as .project, bin folder.

          • Jerry Hernandez

            Finally was able to run. but black screen with the engine meter running at the bottom. Log cat shows..

  • Guest

    Here.

  • Jayson Pinzon

    I had a problem with this.. please help me

    • Vykthur

      Hi Jayson

      When you place your mouse over the errors, what does it say ?
      The message should help in troubleshoooting.

      -V

      • Jayson Pinzon

        it tells me to “create a method in type CCNodeExt” and the other tells that “add cast to eachNode” . If I place the cursor on the line 197 it tells that “it cannot cast from CCNode to CCNodeExt .. thanks for your reply it helps me a lot

      • Guest

        this is the picture

      • Jayson Pinzon

        Hello Vyktur
        I was able to run the program but the output is just a black screen

        • Vykthur

          Hi Jayson

          Have you downloaded all the images and added them to the assets folder of your project ? What device are you using to test ?
          I have got a few reports that devices with very high resolutions sometimes show a black screen.

          Have you tried to download the full sample code from Github and run it ? It may be useful to crossreference your own code with that to detect issues.

          -Vykthur .

          • Jayson Pinzon

            thanks for all your replies to me Vykthur :D, I am using a samsung mobile to run the program I tried all the stuff that your saying to me and it all works.

  • Jayson Pinzon

    can anybody share their code in portrait ? thanks for the reply .. need it badly

  • Symdev

    H,i first congratulations and thank you very much for so complete tutorial.

    I followed the tutorial and it has worked well throughout. I found a bug I would like to fix.

    Normally in the puzzle when a tile is pressed and there an empty cell next to the tile, the tile moves.

    Now when you click on the intersection of two tiles, it happens that the two tiles move, does not always happen but if the size of the tiles is very small often occurs because it is easier to accidentally press two tiles.

    Is there any way around this?
    Can you help me?

    • Vykthur

      Hi … thanks for pointing out this bug. It appears two movetile operations occur at the same time leading to some inconsistency. We can force one to execute completely before the second one does no… to ensure only one tile moves at any given time. I guess you can trying making the moved tile a synchronized function … let me know what you find out ..

      V.

      • Symdev

        I guess I have to put a break in method SlideCallBack. I’ll tell you the result. Thank you.

      • Symdev

        I already found the solution. It works well after adding a “return true” calling method slideCallback in method ccTouchesBegan.

        if(spritePos.contains(location.x, location.y)){

        slideCallback(eachNode);

        return true;

        }