📜 ⬆️ ⬇️

Qook: Port an old toy to Android and share it with the world.

KDPV

In fact, I really like logical toys. No, “three in a row”, “find a similar one” and other “feed the dog” do not interest me much. But the really complicated thing can calmly pull for a couple of weeks. Something like this happened to me in 2004, when a new Sony mobile phone came into my hands. The ability of this T68I to call perfectly, show color pictures and, according to rumors, even sent their contacts via BT passed me unnoticed. Q is not. And how many hours I spent behind a small display, convulsively chasing balls to and fro I already don’t remember. But, I remember well that the idea to write a port of this game for some of the modern platforms has not let me go since its very first Hello World. True, all my attempts to rivet at least some kind of game engine in those good old days broke about ... in general, about something they broke. But now I have been writing in Java for a long time and firmly, and from some (very recently) for Android, so that the idea of ​​a toy port has finally found an opportunity to be realized. Want to see what it is and how it turned out? Then - under the cat.

What's the point of the game?


Q is a very difficult logic game, the point of which is to roll all the colored balls on the playing field into the holes of the same color. Almost like in billiards, yeah. And just like in billiards, the balls move only in a straight line and only until the first obstacle, which can be either a fixed brick or another ball. At the same time, all levels are built in such a way as to exclude the simplest solutions - this is the beauty.

Still do not understand? Well, here's a picture of the first level. Come up, by the way, at your leisure how to pass this very level.
')


That's better? Then let's understand how to write it

What do we write on?


The first desire that occurred to me was to use some kind of physics engine. Well, Unity, for example. However, after I looked at how much such toys weigh and how many batteries they eat - the idea to use the whole engine only to roll the balls beautifully across the field died immediately. But there was an idea to write my own little engine specifically for this game, especially since this part of me did not work in childhood. So we will reinvent our bicycle: with balls and low power consumption. By the way, we will invent it in Java, since Android. Go?

Select the game elements


This is the first thing to do when writing code for anything. Let's see what we have ... so let's look at the picture again ...

Aha On the field we have ... items! Is it logical

public abstract class Item implements Serializable { private Color color; public Item(Color color) { setColor(color); } public Color getColor() { return color; } private void setColor(Color color) { this.color = color; } @Override public String toString() { return "Item{" + "color=" + color + '}'; } } 

What, where are the coordinates? And how does the ball know where it is located there? Not his business.

Now let's go down deeper and see exactly what elements we have here.

Block Everything is simple with him: he is square, gray and does not move anywhere. Neither give nor take - the economy of some not very developed country. True, we must not forget that the block is an element.

 public class Block extends Item { public Block() { super(Color.GRAY); } } 

Ball . The ball is a little more difficult: it is round, multi-colored and always rolls somewhere. And also an element.

 public class Ball extends Item { public Ball(Color color) { super(color); } } 

Hole . Well, or pocket - as you like. We have something in between a ball and a block: seemingly square and motionless, but also multi-colored.

 public class Hole extends Item { public Hole(Color color) { super(color); } } 

So, we already dealt with basic elements. Now, think about where they all lie.

Write level


We will deal with the levels themselves a little later, but for now we need a class that will be responsible for the location of the elements inside the field, since we decided that they themselves know nothing about themselves but the color and the name.

 public class Level implements Serializable { private Item[][] field; private int ballsCount; public Level(Item[][] field) { this.field = field; } } 

Well, the beginning is good. We have some level that stores in itself some two-dimensional array of elements. Since all the balls-blocks-holes we have elements, so it is possible. We need the second variable in order not to count the number of balls left on the field each time. However, once this case is to be considered honest,

 private int countBallsOnLevel(Item[][] field) { int ballsCount = 0; for (Item[] aField : field) { for (int j = 0; j < field[0].length; j++) { if (aField[j] != null && aField[j].getClass().equals(Ball.class)) { ballsCount++; } } } return ballsCount; } 

Quadratic complexity, yeah. That is why I do not want to recalculate this value after the next move. Well, add one line to the constructor

 this.ballsCount = countBallsOnLevel(field); 

So, our level is ready. Now, according to the plan, the most interesting

We write the engine


Let the whole game mechanics we have engaged in a separate special class. Well, for example, Field, which will store the modified configuration of the level, as well as the number of balls remaining on the field

 private Level level; private int ballsCount; public Field(Level level) { this.level = level; this.ballsCount = level.getBallsCount(); } 

Fine. Now let's briefly digress from the engine and write a small enum

 public enum Direction { LEFT, RIGHT, UP, DOWN, NOWHERE } 

Yeah, the direction of movement of the ball. Now let's digress once again and write a very small classic who will keep the coordinates of the desired element on the field. What for? And then write less

 public class Coordinates { private int horizontal; private int vertical; public Coordinates(int horizontal, int vertical) { this.horizontal = horizontal; this.vertical = vertical; } } 

Hooray, finally you can go back to the engine and continue our overwork.

The first thing I want to do is to teach our field to move the balls.

 private Coordinates moveRight(int xCoord, int yCoord) { try { while (level.getField()[yCoord][xCoord + 1] == null) { level.getField()[yCoord][xCoord + 1] = level.getField()[yCoord][xCoord]; level.getField()[yCoord][xCoord++] = null; } } catch (ArrayIndexOutOfBoundsException ex) { } return new Coordinates(xCoord, yCoord); } 

This method, for example, will roll the ball until a decent obstacle is encountered. Well, or until the field is over. By the way, this is probably the only time when the suppression of exceptions is at all somehow justified.

No more difficult than this - other methods are written to move the ball to the left, up and down. It is only necessary to learn how to call these methods somewhere higher.

 private Coordinates moveItem(Coordinates coordinates, Direction direction) { int horizontal = coordinates.getHorizontal(); int vertical = coordinates.getVertical(); if (direction.equals(Direction.NOWHERE) || level.getField()[vertical][horizontal] == null) { return null; } Class clazz = level.getField()[vertical][horizontal].getClass(); if (!clazz.equals(Ball.class)) { return null; } switch (direction) { case RIGHT: return moveRight(horizontal, vertical); case LEFT: return moveLeft(horizontal, vertical); case UP: return moveUp(horizontal, vertical); case DOWN: return moveDown(horizontal, vertical); } return null; } 

Well, here our coordinates are useful. I told you that less writing.

So, more or less learned to ride. Now we will learn to roll. All the same, only the method we will have and return the result of the operation - it turned out to eat the ball or not

 private boolean acceptRight(Coordinates coordinates) { try { int horizontal = coordinates.getHorizontal(); int vertical = coordinates.getVertical(); Item upItem = level.getField()[vertical][horizontal + 1]; Item item = level.getField()[vertical][horizontal]; if (upItem == null || !upItem.getClass().equals(Hole.class) || !(upItem.getColor().equals(item.getColor()))) { return false; } level.getField()[vertical][horizontal] = null; } catch (ArrayIndexOutOfBoundsException ex) { } return true; } 

And exactly the same wrapper level above

 private boolean acceptHole(Coordinates coordinates, Direction direction) { boolean isAccepted = false; switch (direction) { case UP: isAccepted = acceptUp(coordinates); break; case DOWN: isAccepted = acceptDown(coordinates); break; case RIGHT: isAccepted = acceptRight(coordinates); break; case LEFT: isAccepted = acceptLeft(coordinates); break; } if (!isAccepted) { return false; } catchBall(); return checkWin(); } 

After the ball turned out to eat, you need to count the number of remaining ones. No, there is no O (N).

 private void catchBall() { ballsCount--; } 

Why? Because in one move we can move only one ball, which means that we will not be able to roll more. Check that the level is over is made no more difficult.

 private boolean checkWin() { return ballsCount == 0; } 

Well, now we can roll and roll the balls across the field. It remains to learn how to walk

 public boolean makeTurn(Coordinates coordinates, Direction direction) { Coordinates newCoordinates = moveItem(coordinates, direction); return newCoordinates != null && acceptHole(newCoordinates, direction); } 

Nothing new: they took the coordinates with the direction, if it happened, moved the ball to a new place and drove it into the hole if it was found there. If found, they returned true.

Well, that's the whole engine. And was it because of this to cling to some kind of unity here?

Now we just need to teach the phone to show the whole thing on the screen.

We write our view


The main interface element of the Android application is View. View, that is. This is a button, and an input box and ... our playing field. True, it is strange to hope that someone has already written it for us. So you have to do it yourself. To do this, we will create a whole class and inherit it from the built-in View of the androyd, in order to get access to its life cycle, the ability to place this business on the screen and much more.

 public class FieldView extends View { private final double ROUND_RECT_SIZE = 0.15; private final int PADDING_DIVIDER = 4; int paddingSize = 0; private int elementSize; private Field field; private Size fieldSize; private Size maxViewSize; public FieldView(Context context, AttributeSet attrs) { super(context, attrs); } } 


Why do we need constants here, we'll figure it out later, but for now let's think about how big the view should be. It is clear that it should take up as much space on the screen as possible, but not get out of it. And it is clear that the size of the elements must be proportional to the size of the view itself. At the same time, we can’t set something constant - we don’t have to write our view for a couple of thousand different phones. But we can do something with the view when it is placed on the screen. Since in the XML markup it will have the dimensions math_parent, then we will be able to determine this size of runtime.

 public Size countFieldSize() { if (maxViewSize == null) { maxViewSize = new Size(this.getWidth(), this.getHeight()); } int horizontalElementsNum = field.getField()[0].length; int verticalElementsNum = field.getField().length; int maxHorizontalElSize = maxViewSize.getWidth() / horizontalElementsNum; int maxVerticalElSize = maxViewSize.getHeight() / verticalElementsNum; this.elementSize = (maxHorizontalElSize < maxVerticalElSize) ? maxHorizontalElSize : maxVerticalElSize; int newWidth = this.elementSize * horizontalElementsNum; int newHeight = this.elementSize * verticalElementsNum; return new Size(newWidth, newHeight); } 

The size we have is about the same as the coordinates, only needed to store the size of Ox and Oy. The algorithm is simple: we looked at whether someone had determined these dimensions before us, got height and width in pixels, estimated how much one element would occupy horizontally and vertically, chose a smaller one, and even recalculated the size of the view itself by multiplying the size of the element by their number by row and column.

Oh, and do not forget to cause this thing:

 @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); Size countedFieldSize = countFieldSize(); if (fieldSize == null || !fieldSize.equals(countedFieldSize)) { this.fieldSize = countedFieldSize; setFieldSize(this.fieldSize); paddingSize = (int) (Math.sqrt(elementSize) / PADDING_DIVIDER); } } 

What does setFieldSize do? Yes please!

 public void setFieldSize(Size size) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(size.getWidth(), size.getHeight()); params.gravity = Gravity.CENTER_HORIZONTAL; this.setLayoutParams(params); } 

They took a view, and they also attached sizes to it. What do you want?

So, with the size we decided. Now we need to somehow draw the playing field. This is not difficult and is done in onDraw. However, before you draw something you need to find somewhere the game elements themselves.

We draw


The first thing that occurred to me was to get a whole bunch of markup files in a drawable and slip them onto the canvas at the coordinates. Unfortunately, this brilliant idea broke about the impossibility of setting the relative sizes of the elements. That is, I can make rounded corners of the block and set them in dp. And they will actually be rounded. The only problem is that the size of an element changes depending on the number of these elements on the field. And if we have a field of 6 * 6 (the minimum size in the game), the blocks will be square with slightly rounded corners. And if the field is 13 * 13 (maximum size), it will be slightly square balls. Ugly.
However, the idea of ​​painting on canvas with ready-made elements I like more than bothering with some kind of low-level drawing, like drawRect. Let's make a bunch of items?

We will deal with the generation of Drawable in a separate method (although for some reason I wanted to bring it into a separate factory) selectDrawable, which accepts a copy of the element, finds out who he is and drawable for him. For example, a block will be drawn like this:

 Class clazz = item.getClass(); Color color = item.getColor(); if (clazz.equals(Block.class)) { GradientDrawable bgShape = new GradientDrawable(); bgShape.setColor(ContextCompat.getColor(getContext(), R.color.gray)); bgShape.setCornerRadius((float) (elementSize * ROUND_RECT_SIZE)); return bgShape; } 

Well, that and constants come in handy. Now the radius of rounding depends on the size of the element itself. Just what we wanted.

Now let's take a look at how a drawable is built for a ball that is multicolored:

 if (clazz.equals(Ball.class)) { GradientDrawable bgShape = new GradientDrawable(); bgShape.setColor(ContextCompat.getColor(getContext(), R.color.gray)); bgShape.setCornerRadius(elementSize); switch (color) { case GREEN: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.green)); return bgShape; case RED: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.red)); return bgShape; case BLUE: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.blue)); return bgShape; case YELLOW: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.yellow)); return bgShape; case PURPLE: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.purple)); return bgShape; case CYAN: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.cyan)); return bgShape; } } 

Yes, not much harder. First drew a ball, and then poured it with the desired paint. Why switch here and why you can not just set the color that we got out of the ball?
Because it is a different color. The color that is stored in the element is a normal enum, which is from Java, and the one that takes drawable as a color is a normal android resource with a normal string value. For example, here's a red one:

 <color name="red">#D81B60</color> 

Gluing one with the other is a bad idea, because someday it will occur to me that red is not blue enough and it’s time to play around with fonts and have to rewrite the whole thing instead of just fixing the resource file.

Well, for a snack - we build a drawable hole:

 if (clazz.equals(Hole.class)) { GradientDrawable bgShape = new GradientDrawable(); bgShape.setCornerRadius((float) (elementSize * ROUND_RECT_SIZE)); switch (color) { case GREEN: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.green)); return bgShape; case RED: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.red)); return bgShape; case BLUE: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.blue)); return bgShape; case YELLOW: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.yellow)); return bgShape; case PURPLE: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.purple)); return bgShape; case CYAN: bgShape.setColor(ContextCompat.getColor(getContext(), R.color.cyan)); return bgShape; } } 

Again, nothing new: painted a hole, painted it and handed it to the petitioner

So, do not forget anything? Hmm ... Holes, balls, blocks ... And an empty space? What, for example, will happen if null is encountered in the array?

 if (item == null) { GradientDrawable bgShape = new GradientDrawable(); bgShape.setColor(ContextCompat.getColor(getContext(), android.R.color.transparent)); bgShape.setCornerRadius((float) (elementSize * ROUND_RECT_SIZE)); return bgShape; } 

Yes, nothing new will be, because it is exactly the same beautiful rounded square. Sorry, just invisible.

Done, the elements we are able to build. What are we staying at? And ... yes! On drawing them

 @Override protected void onDraw(Canvas canvas) { if (field == null) { return; } for (int i = 0; i < field.getField().length; i++) { for (int j = 0; j < field.getField()[0].length; j++) { Drawable d = selectDrawable(field.getField()[i][j]); d.setBounds(j * elementSize + paddingSize, i * elementSize + paddingSize, (j + 1) * elementSize - paddingSize, (i + 1) * elementSize - paddingSize); d.draw(canvas); } } } 


Here you go. We walked across the field, for each element we find its graphic representation, set its dimensions and indents from each other and draw it on the canvas. By the way, it is interesting that here it is drawable that draws on canvas, and not the canvas draws on itself drawable. In order to do this, I would have to convert the drawable into a bitmap each time, which is a long time.

Let's look at what happened? To do this, we write some test level, where the elements are specified directly in the constructor (remove / remove, do not worry)

But this is better not to write
 public class Level { private Item[][] field; public Item[][] getField() { return field; } public Level() { field = new Item[6][6]; field[0][0] = new Block(); field[0][1] = new Block(); field[0][2] = new Hole(Color.RED); field[0][3] = new Block(); field[0][4] = new Block(); field[0][5] = new Block(); field[1][0] = new Block(); field[1][1] = new Ball(Color.RED); field[1][2] = new Ball(Color.GREEN); field[1][3] = new Ball(Color.YELLOW); field[1][4] = new Ball(Color.CYAN); field[1][5] = new Block(); field[2][0] = new Block(); field[2][1] = new Hole(Color.GREEN); field[2][2] = new Hole(Color.YELLOW); field[2][3] = new Hole(Color.PURPLE); field[2][4] = new Hole(Color.CYAN); field[2][5] = new Hole(Color.BLUE); field[3][0] = new Block(); field[3][1] = new Ball(Color.PURPLE); field[3][5] = new Block(); field[4][0] = new Block(); field[4][1] = new Block(); field[4][3] = new Ball(Color.BLUE); field[4][5] = new Block(); field[5][1] = new Block(); field[5][2] = new Block(); field[5][3] = new Block(); field[5][4] = new Block(); } } 


Now let's hook our view to some kind of activity and run this case.



Finally, it shows something!

And now, inspired by such a beautiful picture, we will teach our interactivity view.

Chasing balls


Since we already have an engine that can move elements, we just have to find a way to call the appropriate methods, somehow interacting with the view.

Interact with the playing field in different ways. If the users are not at all sorry, you can even make the control the same as in the original game - attach a virtual joystick and press it until it's blue. And you can remember that the native gesture for the touch screen, it's still a swipe and brush the balls in the right direction. Understand what we are going to do? Then let's go

In general, for Android there is a built-in GestureManager, but either I didn’t understand how to use it, or it works just like it on my test device, but for some reason I don’t run it so that there are no recognition errors anywhere in my curved handles. came out. So now take and write your own

So, our balls with you can move in exactly four directions: up, down, left and right. True, besides this, they may not move anywhere at all, but this is not at all interesting. So in order to determine the direction of movement of the ball, we need to recognize only 4 simple gestures.

Not really bothering, we start writing another method:

 public Direction getSwipeDirection(float downHorizontal, float upHorizontal, float downVertical, float upVertical) { float xDistance = Math.abs(upHorizontal - downHorizontal); float yDistance = Math.abs(upVertical - downVertical); double swipeLength = getSwipeLength(xDistance, yDistance); if (swipeLength < elementSize / 2) { return Direction.NOWHERE; } if (xDistance >= yDistance) { if (upHorizontal > downHorizontal) { return Direction.RIGHT; } return Direction.LEFT; } if (yDistance > xDistance) { if (upVertical > downVertical) { return Direction.DOWN; } return Direction.UP; } return Direction.DOWN; } 

Direction is Enum, which we described above, and everything else is quite simple: we received 4 coordinates (from where we got them is not important yet) and calculated the distance vertically and horizontally. Then they remembered the course of geometry from high school and found the length of the svayp itself. If it is absolutely small, we will think that the user has nothing to do with it and we will not do anything. If the svayp was good, we will determine where it was so good and return the direction to the user. Cool? I like it too.

Well, let's say, the direction of the svayp we have learned with grief in half. And which of the balls, we forgive, svaypnuli? Let's figure it out.

So, we have the coordinates of the tangency point (we also have the coordinates of the separation point, but what will we do with them?) And from these coordinates we need to find an element ... Hmm.

 public Coordinates getElementCoordinates(float horizontal, float vertical) { float xElCoordinate = horizontal / elementSize; float yElCoordinate = vertical / elementSize; return new Coordinates((int) xElCoordinate, (int) yElCoordinate); } 

Nothing unusual. If all the elements are of the same size, the size of which we know (we have defined ourselves), and we have already calculated the size of the field, it only remains to take and divide. And working with an element according to its coordinates is the task of the engine.

Now we know for sure that we svaypnuli and even guess where. It remains only to transfer the whole thing to the engine and let it rumble. That's just not the task of the view. Her business is to show, and, to process some actions, it would be necessary either in fragments or in activation. We don't have much with fragments, but there is some kind of activism. Hang on the onTouchLictener view.

 private OnTouchListener onFieldTouchListener = new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downHorizontal = event.getX(); downVertical = event.getY(); break; case MotionEvent.ACTION_UP: upHorizontal = event.getX(); upVertical = event.getY(); boolean isWin = fieldView.getField().makeTurn( fieldView.getElementCoordinates(downHorizontal, downVertical), fieldView.getSwipeDirection(downHorizontal, upHorizontal, downVertical, upVertical) ); } 

Here you go. When we touch, we will save the coordinates, when we release the display, we will receive a couple of coordinates, and then - collect everything in a heap and transfer it to the view - let it be disassembled. The view will give the whole thing further, get a boolean result, which is no less than a sign of the completion of the level and will return to us. It only remains to process it, and not to forget to tell the view to redraw.

Add to listener:

 fieldView.invalidate(); if (isWin) { animateView(fieldView); try { levelManager.finishLevel(); openLevel(levelManager.getCurrentLevelNumber()); } catch (GameException ex) { onMenuClick(); } } } return true; 

Checked and even did something. What exactly we have done, we understand a little later, but for now let's play. Remove all unnecessary from our Level, leave only two balls and try to drive one of them into the pocket



Hehe. It even works. Then move on

Looking for levels


Here you go. We wrote the work engine, we learned how to draw it beautifully, now it only remains to decide what we learned to draw. With levels, aha.

Initially, I was not going to draw the levels myself, because our goal is to port the game and not to write a new one. , - Q, , - Sony (, - ) – .

. T68 , - , 2016- … , . .

, , Windows 98 . , , , txt , , , . , , .

, txt- , . – . .

Level Manager


, – , txt- . , , .

- (, ), , - , , ( , ). ,

 public class LevelManager { private static final String LEVELS_FOLDER = "levels"; private static final String LEVEL_FILE_EXTENSION = ".lev"; private static final int EMPTY_CELL = 0; private static final int BLOCK_CELL = 1; private static final int GREEN_BALL_CELL = 2; private static final int RED_BALL_CELL = 3; private static final int BLUE_BALL_CELL = 4; private static final int YELLOW_BALL_CELL = 5; private static final int PURPLE_BALL_CELL = 6; private static final int CYAN_BALL_CELL = 7; private static final int GREEN_HOLE_CELL = 22; private static final int RED_HOLE_CELL = 33; private static final int BLUE_HOLE_CELL = 44; private static final int YELLOW_HOLE_CELL = 55; private static final int PURPLE_HOLE_CELL = 66; private static final int CYAN_HOLE_CELL = 77; private static Context context; private static SharedSettingsManager sharedSettingsManager; private static LevelManager instance; private LevelManager() { } public static LevelManager build(Context currentContext) { context = currentContext; sharedSettingsManager = SharedSettingsManager.build(currentContext); if (instance == null) { instance = new LevelManager(); } return instance; } 

? . ,



- . , . sharedSettingsManager, - ,

, - . Scanner,

 private Scanner openLevel(int levelNumber) throws IOException { AssetManager assetManager = context.getAssets(); InputStream inputStream = assetManager.open( LEVELS_FOLDER + "/" + String.valueOf(levelNumber) + LEVEL_FILE_EXTENSION); BufferedReader bufferedReader = new BufferedReader (new InputStreamReader(inputStream)); return new Scanner(bufferedReader); } 

, . assets , , , , . . , , , .

– Item.

 private Item convertLegendToItem(int itemLegend) { switch (itemLegend) { case EMPTY_CELL: return null; case BLOCK_CELL: return new Block(); case GREEN_BALL_CELL: return new Ball(Color.GREEN); case RED_BALL_CELL: return new Ball(Color.RED); case BLUE_BALL_CELL: return new Ball(Color.BLUE); case YELLOW_BALL_CELL: return new Ball(Color.YELLOW); case PURPLE_BALL_CELL: return new Ball(Color.PURPLE); case CYAN_BALL_CELL: return new Ball(Color.CYAN); case GREEN_HOLE_CELL: return new Hole(Color.GREEN); case RED_HOLE_CELL: return new Hole(Color.RED); case BLUE_HOLE_CELL: return new Hole(Color.BLUE); case YELLOW_HOLE_CELL: return new Hole(Color.YELLOW); case PURPLE_HOLE_CELL: return new Hole(Color.PURPLE); case CYAN_HOLE_CELL: return new Hole(Color.CYAN); } return null; } 

- switch .

– :

 public Level getLevel(int levelNumber) throws IOException { Scanner scanner = openLevel(levelNumber); int levelWidth = scanner.nextInt(); int levelHeight = scanner.nextInt(); Item levelMatrix[][] = new Item[levelHeight][levelWidth]; for (int i = 0; i < levelHeight; i++) { for (int j = 0; j < levelWidth; j++) { levelMatrix[i][j] = convertLegendToItem(scanner.nextInt()); } } Level level = new Level(levelMatrix); sharedSettingsManager.setCurrentLevel(levelNumber); return level; } 

– . Miracles. , , «» , . – ,

 public void finishLevel() { sharedSettingsManager.setCurrentLevel( sharedSettingsManager.getCurrentLevel() + 1 ); if (sharedSettingsManager.getCurrentLevel() > sharedSettingsManager.getMaxLevel()) { throw new GameException(GameExceptionCodes.INCORRECT_LEVEL); } } 

, , , , , . ? , ,



! , .


, . - , .

, Android SharedSettings. , , .

 public static final String LAST_LEVEL = "current_level"; public static final String MAX_LEVEL = "max_level"; public static final String WAS_RAN_BEFORE = "was_ran_before"; private static final String APP_PREFS = "qook_prefs"; public static Context context; public static SharedSettingsManager instance; SharedPreferences sharedPreferences; private SharedSettingsManager() { sharedPreferences = context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE); } 

, . , , – threadsafe sharedsettings - .

, .

Time

 public int getMaxLevel() { return sharedPreferences.getInt(MAX_LEVEL, 1); } 



 public int getCurrentLevel() { return sharedPreferences.getInt(LAST_LEVEL, 1); } 

, . - ,

 private void setMaxLevel(int maxLevel) { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putInt(MAX_LEVEL, maxLevel); editor.apply(); } 


 public void setCurrentLevel(int currentLevel) { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putInt(LAST_LEVEL, currentLevel); editor.apply(); if (getMaxLevel() < currentLevel) { setMaxLevel(currentLevel); } } 

, , – . ?



, . – . «» .



, , . , ,

,
 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/game_activity" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/background" android:gravity="center_vertical" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".ui.activities.LevelActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/level_counter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="16dp" android:paddingTop="5dp" android:text="01 / 60" android:textSize="34sp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="right" android:paddingBottom="10dp"> <ImageButton android:id="@+id/back_level_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="10dp" android:src="@drawable/menu_icon" /> <ImageButton android:id="@+id/reset_level_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="10dp" android:src="@drawable/restore_level" /> <ImageButton android:id="@+id/undo_step_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="10dp" android:src="@drawable/undo_step" /> </LinearLayout> </LinearLayout> <org.grakovne.qook.ui.views.FieldView android:id="@+id/field" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center_horizontal" android:background="@drawable/field_ground" android:foregroundGravity="center" /> </LinearLayout> 


. -: - , , . , !


, , : – , , . ?

 <TextView android:id="@+id/title_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:paddingBottom="16dp" android:text="@string/app_name" android:textAllCaps="true" android:textSize="48sp" android:textStyle="bold" /> <GridView android:id="@+id/level_grid" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="9" android:numColumns="5"> </GridView> 

– . , -

, , , clickListener . Like that:

 public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater vi; vi = LayoutInflater.from(getContext()); @SuppressLint("ViewHolder") View view = vi.inflate(R.layout.level_item, null); Integer currentLevelNumber = getItem(position); if (currentLevelNumber != null) { Button levelButton = (Button) view.findViewById(R.id.level_item_button); if (levelButton != null) { levelButton.setText(String.valueOf(currentLevelNumber)); if (position < maxOpenedLevel) { levelButton.setBackgroundResource(R.drawable.opened_level_item); levelButton.setClickable(true); levelButton.setOnClickListener(clickListener); levelButton.setId(currentLevelNumber); } else { levelButton.setBackgroundResource(R.drawable.closed_level_item); levelButton.setClickable(false); } } } return view; } 

Great. , . , Button :

 public class LevelButton extends Button { @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, widthMeasureSpec); } } 

Hehe. , . .

 @Override public void onResume() { super.onResume(); manager = LevelManager.build(getBaseContext()); View.OnClickListener levelClick = new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(getBaseContext(), LevelActivity.class); intent.putExtra(DESIRED_LEVEL, v.getId()); intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); startActivity(intent); } }; LevelGridAdapter adapter = new LevelGridAdapter(this, R.layout.level_item, getListOfLevelNumbers(), manager.getMaximalLevelNumber(), levelClick); adapter.setNotifyOnChange(false); levelGrid.setAdapter(adapter); levelGrid.setVerticalScrollBarEnabled(false); } 

– . Beauty.


, , , , « », , .


– . : , , – .

, – Google Play, ,

, $25 , , , … «», , ,

Google Play

Here you go. . . , , , « »,

, , GrakovNe

Source: https://habr.com/ru/post/283132/


All Articles