Androidでゲームを開発した最初の経験を得た方法

画像



この記事では、Androidプラットフォーム向けの最初のゲームを作成した経験を共有し、アイデアの誕生から出版までの全過程を説明したいと思います。



背景



2012年以来、私はプログラミング、特にC#でのWindows用プログラムの開発に深く関わってきました。 ずっと、.NETアプリケーションの開発で多くの経験を積み、チームプロジェクトを含むさまざまなプロジェクトに参加しました。 Android向けの開発をしたかったことがわかりました。 C#(Xamarin)またはJavaの選択に直面しました。 私は過去3年間C#でプログラミングを行ってきたので、できるだけ早く開発を開始したかったのですが、新しいIDEとJavaプログラミングのニュアンスを理解したくなかったため、選択は明らかでした。



「何を開発するのか」という考えはすぐに生まれ、VKで音楽を聞いてダウンロードするためのアプリケーションを開発することで構成されました。 アプリケーションはほぼ完成しましたが、いくつかの問題のために公開されませんでした。 6か月後、私は多くの「自由な」時間を過ごし、Android用アプリケーションの開発に戻ることにしました。 1人のプログラマーに会い、Java(Android)とC#(Xamarin)の長所と短所について話し、私は彼のリーダーシップの下でJavaに関する本を勉強し、IDE IntelliJ IDEAを学び始めました。



アイデア、またはそれがすべて始まった場所



開発の原則(私にとっては非常に簡単だった)に2週間慣れた後、馴染みのあるデザイナーは、本を読んで例を繰り返すのではなく、ゲームの開発をすぐに開始し、起こりうるプログラミングの問題を解決することを提案しました。 それはまさに私がやったことであり、ゲームのアイデアを思いついただけでしたが、5分後にデザイナー自身がそれを思いつきました。



ゲームは記憶の発達を目的としており、その本質は次のとおりです:数秒間、プレイヤーはさまざまな形、色、サイズ、場所のいくつかの数字の画像を表示されます。 合格したレベルごとに、プレーヤーは「スター」を受け取ります。これにより、より難しい新しいレベルを開くことができます。



合格レベルの推定値(受け取った星の数)を決定するために、3つのパラメーターを使用することに決定しました:図形の色、その位置、および半径。 ゲームプレイに複雑さを追加するために、プレイヤーは画面上の位置を考慮して、提示されたすべての数字の形状を繰り返す必要があるという条件が導入されました。そうでない場合、レベルは合格しなかったと見なされます。



設計



同じデザイナーがグラフィカルインターフェイスの開発を手伝ってくれましたが、モバイルアプリケーションのUI / UXデザインの開発に参加したのはこれが初めてではありませんでした。 彼はロゴ、アイコンを開発し、原色、フォント、ボタンサイズなどを選択しました。次に、ベクトルで描かれた既製のインターフェイスソリューションを提示しました。 リリース用に準備された資料(「プレビュー」、広告画像など)。



作業の結果はスクリーンショットで見ることができます:
















開発



ゲームの完成したデザインを受け取った後、私はすぐに開発に取り掛かりました。 直面しなければならなかった困難は、図形の描画の実装にのみ関連していました。 合計で、図形を描画するための6つのメソッド(円、正方形、三角形、五角形、星、六角形)を実装する必要があり、そのうちの2つはJavaで既に実装されています。 条件の1つは、図形をすべての方向に中心から描画し、半径のある円に内接する必要があることでした。この円のサイズはユーザー自身が決定しました。 これが私の最初のプロジェクトだったので、既製のライブラリを使用せずに、残りの4つの図を描画する方法を独立して実装することにしました。 図形を描画するには、 canvas.drawPath() (線を描画する方法)を使用しましたが、線のパスが通過するポイントのみを決定できました。 この点で、私は学校のカリキュラムを覚えて、幾何学の教科書を開かなければなりませんでした。



次のニュアンスは、図をいくつかの方法で描くことができるということでした:指定された中心、フレームあり、フレームと中心なし、フレームと中心あり、フレームは実線または破線です。 この問題を解決するために、3つの方法(図、フレーム、フレーム付きの図の描画)でインターフェイスが作成されました。 このインターフェイスは、図形の描画クラスを実装するために使用されました。



図を描くためのインターフェース
public interface DrawFigure { public void draw(Canvas canvas, PointF startPoint, PointF finishPoint, Paint mPaint); public void drawWithBorder(Canvas canvas, PointF startPoint, PointF finishPoint, Paint mPaint, Paint borderPaint); public void drawBorder(Canvas canvas, PointF startPoint, PointF finishPoint, Paint mPaint, Paint borderPaint); }
      
      







三角形描画クラス
 public class DrawThreeAngle implements DrawFigure { private float mRadius; private int[] angles = new int[]{30, 150, 270, 30}; private float[] xValues = new float[4]; private float[] yValues = new float[4]; @Override public void draw(Canvas canvas, PointF startPoint, PointF finishPoint, Paint mPaint) { mRadius = Tools.getRadius(startPoint, finishPoint); Path threeanglePath = new Path(); for (int i = 0; i < angles.length; i++) { xValues[i] = startPoint.x + (float) (mRadius * Math.cos(Math.toRadians(angles[i]))); yValues[i] = startPoint.y + (float) (mRadius * Math.sin(Math.toRadians(angles[i]))); if (i==0){ threeanglePath.moveTo(xValues[i], yValues[i]); } else { threeanglePath.lineTo(xValues[i], yValues[i]); } } threeanglePath.close(); canvas.drawPath(threeanglePath, mPaint); } @Override public void drawWithBorder(Canvas canvas, PointF startPoint, PointF finishPoint, Paint mPaint, Paint borderPaint) { draw(canvas, startPoint, finishPoint, mPaint); drawBorder(canvas, startPoint, finishPoint, mPaint, borderPaint); } @Override public void drawBorder(Canvas canvas, PointF startPoint, PointF finishPoint, Paint mPaint, Paint borderPaint) { for (int i = 0; i < angles.length - 1; i++) { canvas.drawLine(xValues[i], yValues[i], xValues[i + 1], yValues[i + 1], borderPaint); } } }
      
      







スター描画クラス
 public class DrawStar implements DrawFigure { private float mRadius; private int[] angles = new int[]{-18, -54, 270, 234, 198, 162, 126, 90, 54, 18, -18}; private float[] xValues = new float[11]; private float[] yValues = new float[11]; @Override public void draw(Canvas canvas, PointF startPoint, PointF finishPoint, Paint mPaint) { mRadius = Tools.getRadius(startPoint, finishPoint); Path starPath = new Path(); for (int i = 0; i < angles.length; i++) { if (i % 2 == 0) { xValues[i] = startPoint.x + (float) (mRadius * Math.cos(Math.toRadians(angles[i]))); yValues[i] = startPoint.y + (float) (mRadius * Math.sin(Math.toRadians(angles[i]))); } else { xValues[i] = startPoint.x + (float) (mRadius / 2 * Math.cos(Math.toRadians(angles[i]))); yValues[i] = startPoint.y + (float) (mRadius / 2 * Math.sin(Math.toRadians(angles[i]))); } if (i==0){ starPath.moveTo(xValues[i], yValues[i]); } else { starPath.lineTo(xValues[i], yValues[i]); } } starPath.close(); canvas.drawPath(starPath, mPaint); } @Override public void drawWithBorder(Canvas canvas, PointF startPoint, PointF finishPoint, Paint mPaint, Paint borderPaint) { draw(canvas, startPoint, finishPoint, mPaint); drawBorder(canvas, startPoint, finishPoint, mPaint, borderPaint); } @Override public void drawBorder(Canvas canvas, PointF startPoint, PointF finishPoint, Paint mPaint, Paint borderPaint) { for (int i = 0; i < angles.length - 1; i++) { canvas.drawLine(xValues[i], yValues[i], xValues[i + 1], yValues[i + 1], borderPaint); } } }
      
      







レベル実装



ゲームプレイ全体を実装した後、レベルの実装について設定しました。 合計60レベルのさまざまな難易度が作成されました。 レベルの複雑さは、図の数、サイズ、場所によって異なります。 また、交差する図を描く可能性があるため、レベルの複雑さが増しました。



すべてのレベルを格納するために、リストを持つ2つのテーブルで構成されるデータベースが作成されました。 データベース内の図形は座標(図形の中心とその半径上の点)で指定されるため、異なる画面では異なる縮尺で表示されるか、さらにはそれを超えて表示されます。 そのため、異なる画面で同じスケールでレベルをレンダリングする問題を解決する必要がありました。 この問題を解決するために、図形の座標は、いわゆる基準解像度で決定されました。 現在の解像度に対する基準解像度の比率を決定し、すべての図形の座標に計算された係数を掛けることにより、希望するスケールでの表示が実行されます。



データベース構造
 public class LevelsDatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME = "levels.sqlite"; private static final int VERSION = 1; private static final String TABLE_LEVELS = "levels"; private static final String COLUMN_LEVELS_STARS = "stars"; private static final String COLUMN_LEVELS_TIME = "time"; private static final String COLUMN_LEVELS_NUMBER = "number"; private static final String TABLE_FIGURE = "figure"; private static final String COLUMN_FIGURE_TYPE = "type"; private static final String COLUMN_FIGURE_NUMBER = "number"; private static final String COLUMN_FIGURE_CENTER_X = "center_x"; private static final String COLUMN_FIGURE_CENTER_Y = "center_y"; private static final String COLUMN_FIGURE_FINISH_X = "finish_x"; private static final String COLUMN_FIGURE_FINISH_Y = "finish_y"; private static final String COLUMN_FIGURE_COLOR = "color"; private static final String COLUMN_FIGURE_LAYER_INDEX = "layer_index"; private Context mContext; private SQLiteDatabase mDataBase; public LevelsDatabaseHelper(Context context) { super(context, DB_NAME, null, 2); } @Override public void onCreate(SQLiteDatabase db) { mDataBase = db; db.execSQL("create table " + TABLE_LEVELS + " (_id integer primary key autoincrement, " + COLUMN_LEVELS_NUMBER + " integer, " + COLUMN_LEVELS_STARS + " integer, " + " string, " + COLUMN_LEVELS_TIME + " integer)"); db.execSQL("create table " + TABLE_FIGURE + " (_id integer primary key autoincrement, " + COLUMN_FIGURE_NUMBER + " integer, " + COLUMN_FIGURE_TYPE + " string, " + COLUMN_FIGURE_CENTER_X + " real, " + COLUMN_FIGURE_CENTER_Y + " real, " + COLUMN_FIGURE_FINISH_X + " real, " + COLUMN_FIGURE_FINISH_Y + " real, " + COLUMN_FIGURE_COLOR + " integer, " + COLUMN_FIGURE_LAYER_INDEX + " string)"); } }
      
      







詐欺



レベルをテストするときに、交差する数字があるレベルを渡すときに抜け穴が見つかりました。 抜け穴は、レベルをチェックするときに、図が配置されているレイヤーが考慮されないことでした。 その結果、最初に上の図を描画し、次に下の図を描画すると、レベルは合格とみなされます。 この抜け穴をなくすために、変数が配置されるレイヤーの番号を決定する変数がFigureクラスに追加されました。 次の条件も考慮されました。





この問題の解決策は、画像でより明確に示されています。




レベルをチェックするとき、最初にレイヤーに依存しない数字がチェックされ、次に残りの数字のレイヤー番号が描かれた順に比較されました。



カスタムビュー



自明ではない設計を考えると、ほとんどのビューは独立して実装する必要がありました。たとえば、渡されたレベルの結果ウィンドウの進行状況バーなどです。 進行状況バーの外観は標準ではなく、左側(評価を行うパラメーターの名前)と右側(このパラメーターで取得した結果)に2つの見出しがあります。 また、条件の1つは、アニメーション化された塗りつぶしをサポートする機能でした。



プログレスバーの外観




ProgressBarSuccessViewコード
 public class ProgressBarSuccessView extends View { float mPercentValue = 0; String mLeftTitleText = "title"; int mWidth, mHeight; final float mLeftTitleSize = 0.3f, mProgressBarSize = 0.5f, mRightTitleSize = 0.2f; float mBorderMargin, mProgressMargin; RectF mProgressBarBorder = new RectF(), mProgressBarRectangle = new RectF(); int mLeftTitleX, mLeftTitleY, mRightTitleX, mRightTitleY; Paint mTitlePaint, mProgressPaint, mBorderProgressBarPaint; private Rect mTitleTextBounds = new Rect(); public ProgressBarSuccessView(Context context) { super(context); init(null); } public ProgressBarSuccessView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } public ProgressBarSuccessView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(attrs); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; mTitlePaint.getTextBounds(mLeftTitleText, 0, mLeftTitleText.length(), mTitleTextBounds); mLeftTitleX = (int) ((w * mLeftTitleSize - mTitleTextBounds.width()) / 2 - mTitleTextBounds.left); mLeftTitleY = (h - mTitleTextBounds.height()) / 2 - mTitleTextBounds.top; mProgressBarBorder.set(w * mLeftTitleSize, 0 + mBorderMargin, w * (mLeftTitleSize + mProgressBarSize), h - mBorderMargin); } private void init(AttributeSet attrs) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ProgressBarTitle); mLeftTitleText = a.getString(R.styleable.ProgressBarTitle_progress_bar_title); mBorderMargin = getResources().getDimension(R.dimen.border_progress_bar_margin); mProgressMargin = getResources().getDimension(R.dimen.progress_bar_margin); mTitlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTitlePaint.setColor(getResources().getColor(R.color.title_level_success_view)); mTitlePaint.setStyle(Paint.Style.STROKE); mTitlePaint.setTextSize(getResources().getDimension(R.dimen.progress_bar_title_text_size)); mTitlePaint.setTypeface(FontManager.BEBAS_REGULAR); mBorderProgressBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBorderProgressBarPaint.setColor(getResources().getColor(R.color.title_level_success_view)); mBorderProgressBarPaint.setStyle(Paint.Style.STROKE); mBorderProgressBarPaint.setStrokeWidth(2); mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mProgressPaint.setColor(getResources().getColor(R.color.progress_bar_level_success_view)); mProgressPaint.setStyle(Paint.Style.FILL); setWillNotDraw(false); } public void setPercent(float percent) { mPercentValue = Math.round(percent * 100f) / 100f; invalidate(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); float left, top, right, bottom; left = mWidth * mLeftTitleSize + mProgressMargin - mBorderMargin; top = 0 + mProgressMargin; right = mPercentValue > 0 ? left + mWidth * mProgressBarSize * mPercentValue - mProgressMargin - mBorderMargin : left; bottom = mHeight - mProgressMargin; mProgressBarRectangle.set(left, top, right, bottom); canvas.drawText(mLeftTitleText, mLeftTitleX, mLeftTitleY, mTitlePaint); canvas.drawRect(mProgressBarBorder, mBorderProgressBarPaint); canvas.drawRect(mProgressBarRectangle, mProgressPaint); mTitlePaint.getTextBounds(String.valueOf((int) (mPercentValue * 100)), 0, String.valueOf((int) (mPercentValue * 100)).length(), mTitleTextBounds); mRightTitleX = (int) (mWidth - mTitleTextBounds.width() / 2 - mWidth * mRightTitleSize / 2); mRightTitleY = (mHeight - mTitleTextBounds.height()) / 2 - mTitleTextBounds.top; canvas.drawText(String.valueOf((int) (mPercentValue * 100)), mRightTitleX, mRightTitleY, mTitlePaint); } }
      
      







バーの進行状況を埋めるアニメーション
 public static AnimatorSet createProgressAnimationSet(ArrayList<View> views, float[] percentValues) { AnimatorSet animatorSet = new AnimatorSet(); List<Animator> animatorList = new ArrayList<>(views.size()); for (int i = 0; i < views.size(); i++){ float value = percentValues[i]; View view = views.get(i); animatorList.add(createProgressAnimation(view, value)); } animatorSet.playTogether(animatorList); return animatorSet; } private static ObjectAnimator createProgressAnimation(View view, float progressValue){ ObjectAnimator shapeProgressAnimator = ObjectAnimator.ofFloat(view, "percent", 0, progressValue); shapeProgressAnimator.setDuration((long) (1000 * progressValue)); shapeProgressAnimator.setInterpolator(new DecelerateInterpolator()); return shapeProgressAnimator; }
      
      







ヒント



プレーヤーの生活を楽にするために、ポイントについて取得できるヒントが追加されました。 同様に、ポイントはレベルを完了すると授与され、ポイントの数はレベルごとに受け取った星によって異なります。 プレーヤーは、得られたポイントを利用可能な3つのヒントのいずれかに費やすことができます。図、その中心、または図の半径の円を表示します。



作業結果










まとめ



最近投稿されたため、アプリケーションの人気について結論を出すのは時期尚早です。 なんらかの理由で、私はゲームの宣伝に参加する時間がありませんでした。 行われた唯一のこと-ソーシャルでゲームへのリンクを公開しました。 ネットワーク。

Javaの学習にはほとんど時間を費やしませんでした。 そして、文学を読むことは無駄にならないほうがいい、なぜなら 新しい本はありません。また、既存の本は、多くの場合、現代のモバイルアプリケーション開発にもはや関係のない実装を提供しています。



このゲームがヒットか失敗かは不明です。 このゲームを作った後、Androidデバイス用のアプリケーションの開発で良い経験を得ました。これは将来役に立つでしょう。また、長い間欲しかったもので忙しくなりました。 私は現在、次のアプリケーションを開発しており、以前のミスを考慮してプロセスにさらに徹底的に取り組んでいます。 さらに、私はまだモバイルアプリケーションについて多くのアイデアを持っていますが、それは世界中ですぐにわかります。



All Articles