Qook:古いおもちゃをAndroidに移植して世界中の人と共有する

KDPV



実際、私は論理玩具が大好きです。 いいえ、「3列に並んでいる」、「類似したものを見つける」、および「犬に餌をやる」など、あまり興味がありません。 しかし、本当に複雑な仕掛けが数週間落ち着いて引き出される可能性があります。 これは、まったく新しいSony携帯電話が手に入った2004年に私に起こったこととまったく同じです。 このT68Iが優れた電話をかけ、カラー写真を表示し、BTを介して連絡先を送信する機能さえありますが、気づかなかったのです。 しかし、Q-いいえ。 そして、何時間私は小さなディスプレイの後ろに座って、必死にボールを前後に追いかけましたが、もう覚えていません。 しかし、私は非常によく覚えています。最新のプラットフォームのいずれかにこのゲームの移植版を書くという考えは、最初のHello World以来、私を行かせませんでした。 確かに、古き良き時代の少なくとも何らかの種類のゲームエンジンをリベットしようとする私の試みはすべてクラッシュしました...一般に、彼らは何かについてクラッシュしました。 しかし、私は長い間、しっかりとJavaで、そしてAndroidの(ごく最近の)ときから書いたので、おもちゃの移植というアイデアがついに実現する機会を見つけました。 それが何であり、どのように生まれたのかを見たいですか? その後-猫の下。



ゲームの意味は何ですか?



Qは非常に複雑なロジックゲームであり、その意味は、競技場のすべての色のボールを同じ色の穴に転がすことです。 まるでビリヤードのようです。 また、ビリヤードの場合と同様に、ボールは直線でのみ移動し、最初の障害物(固定レンガまたは別のボールのいずれか)までしか移動しません。 同時に、すべてのレベルは最も単純な解決策を排除するような方法で構築されています-これが美しさです。



まだわからない? さて、ここに最初のレベルの写真があります。 ちなみに、あなたのレジャーで、このまさにレベルを渡す方法を考え出してください。







それは良いですか? それからそれを書く方法を理解しましょう。



何を書いているの?



私が最初に思いついたのは、何らかの物理エンジンを使用することでした。 たとえば、Unity。 しかし、そのようなおもちゃの重量とバッテリーの消費量調べたところ 、フィールド全体でボールを美しく転がすためだけにエンジン全体を使用するというアイデアはすぐになくなりました。 しかし、このゲーム専用の小さなエンジンを書くというアイデアが浮かびました。特に子供の頃には得られなかったのはこの部分だったからです。 そこで、ボールと低消費電力で自転車を再発明します。 ところで、AndroidからJavaで発明します。 行こう



ゲーム要素を選択する



これは、何かのコードを記述するときに最初に行うことです。 私たちが持っているものを見てみましょう...ので、もう一度写真を見てください...



うん! フィールドには...要素があります! 論理的ですか?



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 + '}'; } }
      
      





何、座標はどこですか? ボールはどのようにしてそれがどこにあるのかを知るのですか 彼のビジネスではありません。



さて、もう少し詳しく見て、ここにどんな要素があるのか​​見てみましょう



ブロックする 彼にとってすべてがシンプルです。彼は正方形で、灰色で、どこにも移動しません。 与えも受けもしない-あまり発展していない国の経済。 確かに、ブロックが要素であることを忘れてはなりません。



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





ボール 。 ボールはもう少し複雑です。丸く、多色で、常にどこかで転がります。 また、要素。



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





。 まあ、またはポケット-あなたが一番好きなように。 ボールとブロックの間に何かがあります。それは四角くて動きがないように見えますが、マルチカラーでもあります。



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





そのため、すでに基本的な要素を把握しています。 さて、彼らがどこにあるのかを考えてください



ライティングレベル



レベル自体については少し後で説明しますが、現時点では、フィールド内の要素の位置を担当するクラスが必要です。なぜなら、あなたと私はそれら自体が色と名前しか知らないと決めたからです。



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





さて、初めは良いです。 何らかの種類の要素の2次元配列を格納するレベルがあります。 すべてのボール、ブロック、穴に要素があるので、可能です。 毎回フィールドに残っているボールの数をカウントしないために、2番目の変数が必要です。 ただし、正直なところ、このビジネスを数える必要があります



 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; }
      
      





二次の複雑さ、ええ。 そのため、次の移動後にこの値を再集計したくありません。 さて、コンストラクタに1行追加します



 this.ballsCount = countBallsOnLevel(field);
      
      





これで、レベルの準備ができました。 今、計画は最も興味深いです



エンジンを書く



すべてのゲームメカニックに特別なクラスを用意してください。 さて、たとえば、フィールドには、可変レベルの構成と、フィールドに残っているボールの数が格納されます



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





素晴らしい。 エンジンから少し注意をそらし、小さな列挙型を書きます



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





ええ、ボールが動いている方向。 もう一度話を進めて、フィールド上の目的の要素の座標を保存する非常に小さな古典を書きましょう。 なんで? そして、以下を書くために



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





やっと、エンジンに戻って過労を続けることができます。



あなたが最初にしたいことは、ボールを動かすために私たちのフィールドを教えることです。



 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); }
      
      





たとえば、この方法は、ある程度の障害物ができるまでボールを転がします。 さて、またはフィールドが終了するまで。 ちなみに、これはおそらく例外の抑制が何らかの形で正当化される唯一のケースです。



これほど複雑なことはありません-ボールを左、上、下に動かす他の方法が書かれています。 これらのメソッドをどこか上のレベルで呼び出す方法を学ぶ必要があります。



 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; }
      
      





さて、ここで私たちの座標は便利です。 書く量を減らすように言った。



だから、多かれ少なかれ乗ることを学んだ。 今、私たちはロールすることを学びます。 すべてが同じで、メソッドのみが操作の結果を返します-ボールを食べたかどうか



 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; }
      
      





そして、まったく同じラッパーレベルアップ



 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(); }
      
      





ボールが食べられた後でも、残りの数を数える必要があります。 いいえ、O(N)はありません。



 private void catchBall() { ballsCount--; }
      
      





なんで? なぜなら、1回の移動で1つのボールしか移動できないからです。これは、もはや転がることができないことを意味します。 レベルが完了したことを確認することはもう難しくありません



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





さて、今私たちはフィールドの周りにボールを転がして転がすことができます。 歩く方法を学ぶことは残っている



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





新しいことはありません。方向が判明した場合は方向と座標を取り、ボールが見つかった場合は、ボールを新しい場所に移動し、穴に押し込みました。 見つかった場合、trueを返します。



さて、それがエンジン全体です。 このため、ここで団結しがみつく価値はありましたか?



次に、画面にすべてを表示するように電話を教える必要があります。



ビューを書く



Androidのアプリケーションインターフェイスの主要な要素はViewです。 ビュー、つまり。 これはボタンであり、入力フィールドであり、...私たちの競技場です。 確かに、誰かが私たちのためにすでにそれを書いていることを願うのは奇妙です。 だからあなたは自分でそれをしなければなりません。 これを行うには、クラス全体を作成し、Androidのビルトインビューから継承して、そのライフサイクル、このビジネスを画面に配置する機能などにアクセスします。



 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); } }
      
      







ここで定数が必要な理由は後で説明しますが、ここでは、ビューの大きさについて考えてみましょう。 画面上で可能な限り多くのスペースを占有する必要があることは明らかですが、そこからクロールしないでください。 また、要素のサイズはビュー自体のサイズに比例する必要があることは明らかです。 同時に、何かを一定に設定することはできません。数千台の異なる携帯電話の下で独自の見解を書くことはできません。 ただし、画面に配置するときに、ビューで何かを行うことができます。 XMLマークアップにmath_parentディメンションがあるため、このサイズのランタイムを決定できます。



 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); }
      
      





サイズは座標とほぼ同じで、OxとOyに応じたサイズを保存するためにのみ必要です。 アルゴリズムは簡単です:誰かがこれらのサイズを決定しているかどうかを調べ、ピクセル単位で高さと幅を取得し、1つの要素が水平および垂直に占める量を計算し、小さい要素を選択し、要素サイズにその数を掛けてビュー自体のサイズを再計算しました行と列で。



そして、まあ、このことを呼び出すことを忘れないでください:



 @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); } }
      
      





setFieldSizeは何をしますか? はい、お願いします!



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





彼らは眺めを取り、それに寸法を付けました。 何が欲しかった?



したがって、サイズを決定しました。 ここで、何らかの形で競技場を描く必要があります。 複雑ではなく、onDrawで行われます。 確かに、何かを描く前に、ゲーム要素自体をどこかに見つける必要があります。



描く



私が最初に思いついたのは、たくさんのマークアップファイルをドロアブルで取得し、それらを座標でキャンバスに配置することでした。 残念ながら、この素晴らしいアイデアは、要素の相対的なサイズを設定することが不可能であることを打ち破りました。 つまり、ブロックの角を丸くして、dpに設定できます。 そして、それらは実際に丸められます。 唯一の問題は、フィールド上の同じ要素の数に応じて、要素のサイズが異なることです。 また、フィールドが6 * 6(ゲームの最小サイズ)の場合、ブロックはわずかに丸みを帯びた正方形になります。 そして、私たちが持っているフィールドがすでに13 * 13(最大サイズ)である場合-それはわずかに正方形のボールになります。 glyい。

ただし、drawRectのような何らかの低レベルの描画に煩わされるよりも、既成の要素でキャンバスに描画するというアイデアが好きです。 要素をたくさんやってみましょうか?



Drawableを生成するための別のメソッドがあります(何らかの理由で別のファクトリーに移動したかったのですが)selectDrawableは、要素のインスタンスを取得し、それが誰であるかを見つけて、それを描画可能にします。 たとえば、ブロックは次のように描画されます。



 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; }
      
      





さて、定数が役に立ちました。 これで、丸み付け半径は要素自体のサイズに依存します。 まさに私たちが望んだもの。



次に、ボール用のドロアブルがどのように構築されているかを見てみましょう。



 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; } }
      
      





はい、それほど複雑ではありません。 最初に彼らはボールを描き、それから彼らは必要なペンキでそれを注いだ。 なぜスイッチがあり、ボールから出たものだけで色を設定できないのですか?

これらは異なる色だからです。 要素に格納される色はJavaからの通常の列挙型であり、色としてdrawableを受け入れるのは、通常の文字列値を持つ通常のAndroidリソースです。 たとえば、これは少し赤いものです:



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





あるものを他のものと接着するのは悪い考えです。ある日、赤では青が足りず、フォント全般をいじって、リソースファイルを修正するのではなく、全体を書き直さなければならないと思い浮かぶでしょう。



さて、初心者向け-穴からドロアブルを構築します。



 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; } }
      
      





繰り返しますが、新しいものは何もありません。穴を描いて、塗装し、サプリカントに渡しました。



それで、あなたは何かを忘れましたか? うーん...穴、ボール、ブロック...そして空の場所? たとえば、配列でnullが検出されるとどうなりますか?



 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; }
      
      





はい、これはまったく同じ美しい丸い正方形であるため、新しいものは何もありません。 すみません、ただ見えません。



完了、要素を構築できます。 そこで何を止めましたか? ああ...はい! それらを描く方法について



 @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); } } }
      
      







よくここに。 フィールドを歩き回り、各要素について、そのグラフィック表現を見つけ、そのサイズとインデントを相互に設定し、キャンバスに描画します。 ちなみに、ここではキャンバス上に描画するドローアブルであり、キャンバス自体にドロウアブルを描画しないのは興味深いことです。 これを行うには、毎回ドロアブルをビットマップに変換する必要があり、これは長い時間です。



何が起こったのか見てみましょうか? これを行うには、コンストラクターで要素が直接設定されるテストレベルを作成します(削除して削除しますので、心配しないでください)



それで、書かない方がいい
 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(); } }
      
      







そして今、私たちは自分の見解をある種の活動に結び付けて、このビジネスを始めます







最後に、それは何かを示しています!



そして今、そのような美しい写真に触発されて、私たちは対話性の私たちの見解を教えます



ボールを打ちます



要素を移動する方法を知っているエンジンが既にあるので、ビューと何らかの形で相互作用する適切なメソッドを呼び出す方法を見つけることしかできません。



さまざまな方法で競技場と対話します。 ユーザーがまったく気に入らない場合は、元のゲームと同じようにコントロールを作成することもできます。仮想ジョイスティックを取り付けて、青色になるまでクリックします。 また、タッチスクリーンのネイティブジェスチャはまだ正しい方向にスワイプおよびボールをスワイプしていることを思い出すことができます。 わかった、何をするつもりだったの? それでは行こう



一般に、Androidには組み込みのGestureManagerがありますが、使用方法がわからなかったか、テストデバイスで動作しましたが、何らかの理由で、曲線ハンドルを認識できるように起動しませんでした。出てきた。 それでは、自分で書いてみましょう



したがって、ボールは正確に4つの方向(上下左右)に移動できます。 確かに、これに加えて、彼らはどこにもまったく動かないかもしれませんが、これはまったく面白くありません。 したがって、ボールの動きの方向を判断するには、4つの簡単なジェスチャーだけを認識する必要があります。



本当に気にすることなく、別のメソッドを書き始めます。



 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; }
      
      





方向は上で説明したEnumであり、他のすべては非常に単純です。4つの座標を取得し(取得元はまだ重要ではありません)、垂直方向と水平方向の距離を計算しました。 その後、彼らは高校の幾何学コースを思い出し、スワイプ自体の長さを見つけました。 それが非常に小さい場合、ユーザーはそれとは何の関係もないと考え、何もしません。 svaypが良かった場合、どこが良かったのかを判断し、ユーザーに方向を返します。 かっこいい 私も好きです。



さて、悲しみでスワイプの方向を半分に決定することを学んだとしましょう。 申し訳ありませんが、どのボールをスヴァイプヌルしますか? 正しくしましょう。



したがって、接点の座標があります(分離点の座標もありますが、それをどうしますか?)そして、これらの座標から、要素を見つける必要があります...うーん。



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





異常なことは何もありません。 すべての要素が同じサイズであり、サイズがわかっている(自分で決定した)場合、およびフィールドのサイズをすでに計算している場合は、取得および分割するだけです。 そして、その座標によって要素を操作することはエンジンのタスクです



これで、私たちは間違いなく、どこにいるのかを推測できます。 すべてをエンジンに転送し、それを鳴らすだけです。 しかし、これはビューのタスクではありません。 彼女の仕事は見せることであり、いくつかの行動を処理するためには、断片的にまたは活動的に必要です。 フラグメントはまばらですが、いくつかのアクティビティがあります。 onTouchLictenerビューにハングアップします。



 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) ); }
      
      





よくここに。 タッチすると、座標が保存され、ディスプレイを離すと、さらに数個の座標が取得されます。次に、すべてをヒープに収集し、ビューに渡します。 ビューはさらにすべてを提供し、ブール値の結果を取得しますが、これは多かれ少なかれではありませんが、レベルの完了の兆候は私たちに戻ります。 それを処理するためだけに残り、再描画する必要があることをビューに伝えることを忘れないでください。



リスナーに追加します。



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





私たちはチェックして、何かをしました。 正確に何をしたか、少し後で理解しますが、今のところは、いじってみましょう。 レベルから余分なものをすべて削除し、2つのボールだけを残して、そのうちの1つをポケットに入れます







へへ。 それも動作します。 次に進む



レベルを探しています



よくここに。 実用的なエンジンを作成し、それを美しく描画することを学びました。今では、正確に描画することを学んだことを決定するだけです。 レベルで、ええ。



私たちの目標はゲームを移植することであり、新しいものを書くことではないため、最初は自分でレベルを描画するつもりはありませんでした。 だから、元のQをどこかに持って行き、よく振って、そこから3〜4人のソニーの弁護士を外し(まあ、それはまだ彼女のおもちゃです)、出来上がり-完了です。



それは、元のおもちゃの検索がそこで終わっていないだけです。 T68自体は長らく消えており、何らかの理由でネットワーク上に写真がありませんでした。2016年に別の元のデバイスを見つけました...一般に見つけるのは問題です。悲劇。



しかし、オリジナルを探していたときに、Windows 98でこのおもちゃの移植版に偶然出くわしまし。そして、このおもちゃのレベルがオリジナルから取り除かれているだけでなく、各シンボルがブロック、ボール、または穴のいずれかを表す通常のtxtファイルにもあることに気付いたとき、私はどれほど驚きましたか。著者と話をして、使用するための同意を確保した後、私は彼らを明確な良心を持って自分自身に連れて行き、彼らと何をすべきかを理解しようとしました。



そのように、あなたは確かにtxtファイルを取り、おもちゃに入れることができます。しかし、私のエンジンをそれらと連携させるには、まだ試してみる必要があります。そうでなければなりません。やってみます



ライティングレベルマネージャー



それは実際には美しく聞こえます-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について、別の時間をお話ししますが、今はマネージャーにレベルを開き、そこから適切なオブジェクトを作成するように教え



ましょう。データストリームからさまざまな数字や文字を取得するために、スキャナーがありますので、ファイルに設定します



 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); }
      
      





はい、ほとんど忘れていました。私たちが持っているすべてのレベルはアンドロイド資産に保存されていますが、コンテキストを持っている場合にのみ問題をなくすことができます。そして、彼らは彼らの番号によって呼び出されます。したがって、必要なレベルの番号を転送するだけで、目的のファイルで動作する既製のスキャナーを取得できます。



次に、凡例要素をアイテムに変換します。



 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; }
      
      





1つの大きなスイッチと複雑なものはありません。



最後に、レベル全体を処理する方法を学習します。



 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; }
      
      





私たちは数を取りました-レベルを返しました。奇跡 しかし、レベルを開くことに加えて、ボールが残っていない瞬間に「完了」する必要もあります。ボールがそこに残っているかどうかを判断するのはエンジンのタスクですが、このように突然終了したレベルを処理します



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





ええ、彼らはこのレベルが合格したことをメモし、現在のレベルの番号を変更し、すべてのレベルが終了した場合、例外を吐き出しました。甘い?次に、たとえば第2レベルの







Xeでおもちゃを実行してみてくださいそして真実は、それが機能するということです。次に、ユーザーの実績を保存する方法を学習します



別のマネージャー



ユーザーが毎回すべてのレベルを強制されると、ユーザーは怒って不満になります。そのため、開始と終了の間にデータが失われないように、現在のレベルと最後に使用可能なレベルの値を何らかの方法で保存する必要があります。



このような単純なものを保存するために、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); }
      
      





また、シングルトン、ええ。私たちがどこに突入したかは決してわかりませんが、何らかの理由でスレッドセーフ自体が共有設定を共有しているかどうかを調べるために、特別な欲求はありません。



ここで、現在および最大のレベルを提供する方法を学びます。







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





そして2



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





今、それらを書き戻してみてください。2つの別々のメソッドの上のどこかを呼び出すことは特に望まないため、これを実行しましょう。



 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>
      
      







まあ、私たちはおもちゃ自体を理解しました。あとは、さらに2、3の画面を追加し、任意のレベルを選択し、ランドスケープマークアップを追加して、これを脇に置くだけです。ささいなこと!



レベルのメニューを書く



このビジネスには別の画面が関与します。これは退屈以上に見えます。各レベルは小さな正方形で、開いているがまだ開いていないさまざまな色のレベルで、開いているレベルをクリックすることはできません。マークアップを作成しますか?



 <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をアタッチして親に詰め込むアダプターを作成する必要があります。このようなもの:



 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; }
      
      





わあすべてのレベルボタンを正方形にするだけです。これを行うには、独自のボタン継承者を作成し、いくつかの魔法を追加します。



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





へへ。なんという幅、そのような高さ。誰も気づきませんでした。アクティビティを作成するときにすべてを呼び出すだけです



 @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); }
      
      





ボタンをクリックしました-レベルが開きました。 美人



遊ぶ



メインメニュー、ヘルプ、そしてもちろん「作者について」をゲームに追加したら、ここで得たものをプレイしてみましょう。





私の歯から跳ね返るのは私の最初の3つのレベルです-coli痛になりました。しかし、それは機能します。両方のレベルが保存され、さらに3つのアセンブリが戻るように、ディスプレイを反転してもアプリケーションがドロップされず、全体がきれいに見えます。



さて、このことは機能するので、この良さをGoogle Playに投稿し、好きな人を見つけて



登録し、25ドルのGoogleを支払い、しばらく待って、プロジェクトを作成し、データフィールドに入力します... 、こんなかわいいダイ



Google Playで利用可能



よくここに。おもちゃを書きました。それを通過して、別の子供時代の夢が実現したことをマークするだけです。そして今、あなたの許可を得て、特に鋭い知人がその不在のために私を完全に引き裂くまで、「最後の動きをキャンセル」ボタンの書き込みを終了します。



敬具テトリスを洗濯機GrakovNeに移植します



All Articles