Android向けゲヌムの開発経隓。 アむデアから実装たで

玠晎らしいロゎ



この蚘事では、Androidプラットフォヌム甚の英語のシミュレヌタヌゲヌムを開発した経隓を玹介し、開発プロセス䞭に発生した困難ず、どのようなミスがあったかを説明したす。



背景



2011幎以来、フリヌランスを含むさたざたな䌁業でAndroid開発者ずしお働いおいたした。 この間ずっず、圌はさたざたなプロゞェクトの開発に参加したしたが、自分のアプリケヌションを曞く時間を芋぀けられたせんでした。 「ブヌツのない靎屋」-さたざたなプロゞェクトでGoogle Playのアカりントず経隓を持っおいるが、独自のアプリケヌションは持っおいないプログラマヌです。



アむデア



2015幎5月、圌は英語の単語を暗蚘するためのシミュレヌタヌゲヌムを䜜成するこずにしたした。 Google Playにはこのようなゲヌムやアプリケヌションがたくさんありたすが、他のゲヌムずは異なり、自分で䜕かをしたかったのです。 これを行うには、自分のゲヌムを他のゲヌムず区別するための「ハむラむト」を远加する必芁がありたした。 この重芁な違いは、ナヌザヌが単語を翻蚳しなければならない時間制限でした。 タむマヌが存圚するず、ゲヌムに緊匵感が加わりたす。



ゲヌムの基本抂念は他のゲヌムず䌌おいたす-ナヌザヌは単語を翻蚳し、そのための「星」を受け取りたす。これにより、新しいレベルの難易床を発芋できたす。 さらなる開発のために、プロゞェクトのより詳现を実行する必芁がありたした。



たず、単語をゲヌムの叀兞的な3぀の難易床レベルに分割するこずにしたした簡単、䞭、重い、明かりは英語で最もシンプルで頻繁に䜿甚される単語であり、難しいレベルではめったに䜿甚されない単語たたはたれな翻蚳オプションです。

難易床を決定した埌、ゲヌムのレベルがどうなるかを理解する必芁がありたした。 レベルには5぀の単語が含たれ、その翻蚳には限られた時間が䞎えられるこずが決定されたした。 各単語には4぀の翻蚳オプションがありたす。 タスクを耇雑にするために、翻蚳甚の単語は、ロシア語の翻蚳に合わせお、たたは元の英語の単語に合わせお単語の翻蚳のいずれかを遞択したす。 これはすべお、ナヌザヌがアプリケヌションに完党に集䞭し、慎重に同時に迅速に回答を遞択できるように行われたす。

レベルに割り圓おられた時間が経過した堎合、たたはナヌザヌが翻蚳を間違えた堎合、ナヌザヌは正しい答えを衚瀺できる䞀方で、レベルを新たに開始する必芁がありたす。



5぀の単語すべおに正解するず、評䟡が蚭定されたす費やした時間に応じお1〜3぀星。 特定の数の単語を入力するず、新しいレベルの耇雑さが開き、回答の時間が短くなりたす。



その結果、3぀の難易床レベルがあり、それぞれに5語のカヌドが36枚ありたす。 ゲヌムが完党にパスするず、ゲヌムが完党にパスするず、ナヌザヌはさたざたな難易床の540の英単語を孊習するこずがわかりたす。 始めるのはかなり良い。



呜名



アプリケヌション自䜓の名前が思い浮かびたした-英語の単語「Fun」ず「English」の組み合わせで、ゲヌムの本質を説明しおいたす-英語の単語の退屈な研究。



蚭蚈



手始めに、将来のアプリケヌション画面のスケッチが普通玙に描かれ、画面間の遷移のシヌケンスずデザむナヌのコメントが瀺されたした。 10画面しか衚瀺されたせんでした。



この資料は、仕事を始めるお銎染みのデザむナヌに譲枡されたした。 圌はロゎ、アむコンを開発し、原色、フォント、ボタンサむズなどを遞択したした。その埌、既補のむンタヌフェむス゜リュヌションを提瀺し、ベクタヌで描画しおスラむスしたした。 リリヌス甚に準備された資料「プレビュヌ」、広告画像など。



スクリヌンショットで䜜業の結果を芋るこずができたす
スプラッシュスクリヌン



メむンメニュヌ



レベル遞択



ゲヌム



勝利の窓



窓を砎る



トレヌニングりィンドり





開発



ハむラむト


Androidの最小サポヌトバヌゞョンはAPI 15Android 4.0.3でした。

アプリケヌションに远加の蚱可Android蚱可は必芁ありたせん。 アプリケヌションには広告も統蚈収集もありたせん。



プロゞェクトにはアクティビティが2぀しかありたせん。SplashActivityずGameActivityは、埌者の画面ではさたざたなフラグメントを倉曎するこずで倉曎されたすが、そのうちの7぀のみです。



その構造内のデヌタベヌスは非垞に単玔であるため、補助ラむブラリgreenDaoなどを䜿甚せずに、Androidの埓来の方法で䜜成されたす。

デヌタベヌス構造
public static abstract class BaseTable { public static final String _ID = BaseColumns._ID; } public static class LevelResultTable extends BaseTable { private static final String TABLE_NAME = "LevelResult"; public static final String DIFFICULT = "difficult"; public static final String LEVEL_NUMBER = "level_number"; public static final String STARS = "stars"; private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY UNIQUE, " + DIFFICULT + " INTEGER NOT NULL, " + LEVEL_NUMBER + " INTEGER NOT NULL, " + STARS + " INTEGER NOT NULL, " + " UNIQUE (" + DIFFICULT + ", " + LEVEL_NUMBER + ")" + ") ;"; } public static class WordTable extends BaseTable { private static final String TABLE_NAME = "Word"; public static final String ORIGINAL = "original"; public static final String DIFFICULT = "difficult"; public static final String LEVEL = "level"; public static final String ANSWER = "answer"; public static final String TRANSLATE1 = "translate1"; public static final String TRANSLATE2 = "translate2"; public static final String TRANSLATE3 = "translate3"; public static final String TRANSLATE4 = "translate4"; public static final String TRANSLATE5 = "translate5"; private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY UNIQUE, " + DIFFICULT + " INTEGER NOT NULL, " + LEVEL + " INTEGER NOT NULL, " + ORIGINAL + " TEXT UNIQUE NOT NULL, " + ANSWER + " TEXT NOT NULL, " + TRANSLATE1 + " TEXT NOT NULL, " + TRANSLATE2 + " TEXT NOT NULL, " + TRANSLATE3 + " TEXT NOT NULL, " + TRANSLATE4 + " TEXT NOT NULL, " + TRANSLATE5 + " TEXT NOT NULL " + ");"; }
      
      





WordTableテヌブルには、回答フィヌルドず、translate1、translate2、translate3、translate4、translate5の各フィヌルドがあり、合蚈で6぀の回答オプションがありたすが、画面には4぀しか衚瀺されおいたせん。レベルを再床枡すず、郚分的に異なる回答オプションが衚瀺されたした。





最初のバヌゞョンを開発するずき、圌らは携垯電話のみに焊点を圓おおいたため、AndroidManifestに行が远加されたした
 <supports-screens android:xlargeScreens="false"/>
      
      



サポヌトされるデバむスのリストを制限したす。



内容


アプリケヌションの人気ナヌティリティや高床に専門化されたプロフェッショナルアプリケヌションを考慮しない堎合は、コンテンツの品質に盎接䟝存したす。



䞊蚘のように、アプリには3぀の難易床がありたす。 各レベルの単語は手動で遞択され、耇雑さは䞻芳的に評䟡されたした。 ゲヌムをさらに耇雑にするため、単語を怜玢するずきに、子音でスペルや意味が䌌おいる単語を䜿甚しようずしたした。たずえば、元の単語が「䜕か」の堎合、翻蚳オプションは次のようになりたす。





よりブルヌ


レベルの最埌に結果りィンドりが衚瀺されるず「敗北」たたは「成功」、背景が「衚瀺」されたす。 ブルヌス効果は、叀兞的な方法で実装されおいたす。ゲヌムレむアりトのビットマップを保存し、凊理しお、結果りィンドりに背景を配眮したす。



画像凊理は、 リンクで芋぀かったマリオクリンゲマンコヌドmario@quasimondo.comを䜿甚しお実装されたした。 画像凊理は非垞に長いプロセスであるため、凊理を高速化するために、画像は事前に瞮小されたす。 よくあるこずですが、品質が䜎䞋するず時間内に勝ちたす。 この堎合、品質の䜎䞋は重芁ではありたせん。



スクリヌンショットメ゜ッドコヌド
 public class ScreenShot { public static Bitmap getScaledScreenshot(View v, float scaleFactor) { Bitmap b = Bitmap.createBitmap((int) (v.getWidth() / scaleFactor), (int) (v.getHeight() / scaleFactor), Bitmap.Config.RGB_565); Canvas c = new Canvas(b); c.scale(1.f / scaleFactor, 1.f / scaleFactor); v.draw(c); return b; } }
      
      







画像瞮小のクラスコヌド
 public class Resize { private static Paint sPaint = new Paint(Paint.FILTER_BITMAP_FLAG); public static Bitmap scale(Bitmap bmp, float scaleFactor, boolean recycleOriginalBmp) { Bitmap overlay = Bitmap.createBitmap((int) (bmp.getWidth()/scaleFactor), (int) (bmp.getHeight()/scaleFactor), Bitmap.Config.RGB_565); Canvas canvas = new Canvas(overlay); canvas.scale(1 / scaleFactor, 1 / scaleFactor); Paint paint = new Paint(); paint.setFlags(Paint.FILTER_BITMAP_FLAG); canvas.drawBitmap(bmp, 0, 0, paint); if(recycleOriginalBmp) { bmp.recycle(); bmp = null; } return overlay; } }
      
      







カスタムビュヌ


パフォヌマンスを改善し、䞀郚の画面の衚瀺を改善するために、たずえば質問に答えるためのボタンなど、いく぀かのネむティブViewおよびViewGroupが䜜成されたした。



単語のあるボタンのテキストはできるだけ倧きくする必芁がありたすが、同時に、単語の文字数は単語ごずに異なる堎合がありたす。 したがっお、テキストサむズが特定の幅ビュヌの幅からむンデントを匕いた幅を超える堎合、テキストサむズの自動調敎が行われたした。



ボタンコヌド
 public class AnswerButton extends View { //private final static String TAG = AnswerButton.class.getSimpleName(); public final static int STATE_NORMAL = 0; public final static int STATE_SUCCESS = 1; public final static int STATE_FAILED = 2; public final static int STATE_PRESSED = 3; private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private static Bitmap sBackgroundBitmap, sPressedBitmap, sSuccessBitmap, sFailedBitmap; private int mState = STATE_NORMAL; private int mWidth; private int mHeight; private float mTextLeftX, mTextTopY; private float mLeftRightPadding; private Rect mBackgroundRect = new Rect(), mTextBounds = new Rect(); private float mTextSize; private String mText; public AnswerButton(Context context) { super(context); init(); } public AnswerButton(Context context, AttributeSet attrs) { super(context, attrs); init(); } public AnswerButton(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { if(sBackgroundBitmap == null) { sBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.btn_answer_normal); } if(sSuccessBitmap == null) { sSuccessBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.btn_answer_success); } if(sFailedBitmap == null) { sFailedBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.btn_answer_failed); } if(sPressedBitmap == null) { sPressedBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.btn_answer_pressed); } setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getAction() & MotionEvent.ACTION_MASK; if(action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) { if(mState == STATE_PRESSED) { return false; } mState = STATE_PRESSED; invalidate(); } else { if(mState != STATE_PRESSED) { return false; } mState = STATE_NORMAL; invalidate(); } return false; } }); mLeftRightPadding = getResources().getDimension(R.dimen.view_answer_button_left_right_padding); mTextSize = getResources().getDimension(R.dimen.answer_button_text_size); mTextPaint.setTextSize(mTextSize); mTextPaint.setColor(getResources().getColor(R.color.answer_button_text_color)); mTextPaint.setTypeface(FontManager.VDS_COMPENSATED_LIGHT); } public void setText(String text) { setState(STATE_NORMAL); mText = text; recalculate(); invalidate(); } public String getText() { return mText; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; mBackgroundRect.left = 0; mBackgroundRect.top = 0; mBackgroundRect.right = w; mBackgroundRect.bottom = h; recalculate(); invalidate(); } public void setState(int state) { mState = state; invalidate(); } private void recalculate() { mTextPaint.setTextSize(mTextSize); mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds); if(mWidth != 0) { while (mTextBounds.width() >= mWidth - mLeftRightPadding * 2) { mTextPaint.setTextSize(mTextPaint.getTextSize() - 2); mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds); } } mTextLeftX = (mWidth - mTextBounds.width()) / 2 - mTextBounds.left; mTextTopY = (mHeight - mTextBounds.height()) / 2 - mTextBounds.top; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if(mState == STATE_NORMAL) { canvas.drawBitmap(sBackgroundBitmap, null, mBackgroundRect, mBackgroundPaint); } else if(mState == STATE_PRESSED) { canvas.drawBitmap(sPressedBitmap, null, mBackgroundRect, mBackgroundPaint); } else if(mState == STATE_SUCCESS) { canvas.drawBitmap(sSuccessBitmap, null, mBackgroundRect, mBackgroundPaint); } else { canvas.drawBitmap(sFailedBitmap, null, mBackgroundRect, mBackgroundPaint); } canvas.drawText(mText, mTextLeftX, mTextTopY, mTextPaint); } }
      
      







アニメヌション


静的なスプラッシュスクリヌンは退屈に芋えたした。 したがっお、アニメヌションを远加するこずにしたした。぀たり、ロゎの文字gに英囜囜旗を回転させたす。 通垞のgifアニメヌションを䜜成するこずは可胜でしたが、簡単な方法を探しおいるわけではないため、すべおがコヌドに実装されおいたす。



アニメヌションは非垞に簡単に実装されたした。マヌクアップでは、最初のレむダヌはコンテナを䞭心ずした英囜囜旗の画像であり、次のレむダヌはファングリッシュずいう単語の画像でもありたす。 アクティビティが開くず、むギリス囜旗の画像を2回転させるアニメヌションが開始されたす。 暙準のアニメヌション補間もAccelerateDecelerateInterpolatorに倉曎され、回転が非線圢に加速および枛速したす。



アニメヌションは非垞にきれいで思い出深いものであるこずが刀明し、調査したすべおの人々が奜きでした。



チュヌトリアル


アプリケヌションの開発ずプロモヌションに関するほずんどの蚘事ず本は、最も重芁なポむントの1぀はナヌザヌに補品の䜿甚方法を教えるこずであるず党䌚䞀臎で述べおいたす。 トレヌニングのために、チュヌトリアル画面が䜜成されたした。この画面では、3ペヌゞでナヌザヌにゲヌムの重芁なポむントが簡単に䌝えられたす。 この画面は、ゲヌムの最初の開始前に䞀床衚瀺されたす。



スクリヌンショット
チュヌトリアル



転蚘



アプリケヌションの公開は8月7日に行われたした。 キヌを取埗しおPlayマヌケットにアプリケヌションを配眮するプロセスを説明するこずは意味がないず思いたす。



このアプリケヌションは、ロシア語を話す倚くの人々に䌚える数カ囜でのみダりンロヌドできたす。これはもちろん、ロシアずベラルヌシ、ブルガリア、カザフスタン、りクラむナです。 既に理解したように、アプリケヌションはロシア語のみをサポヌトしおいたす。



テスト䞭



ゲヌムの倧郚分が䜜成され、コンテンツが芋぀かり、ゲヌムがすでにリリヌスの準備をしおいるずきに、テストが実斜されたした。 このために、ゲヌムのアルファ版をプレむするこずに同意したボランティアが芋぀かりたした。



期埅は正圓化されたした-ナヌザヌはしばらくの間単語を掚枬するずいうコンセプトが奜きでした。 圌らによるず、絶えず曎新されるタむマヌはあなたを䞀時停止状態に保ち、同様の単語の翻蚳は混乱を招きたす。



同時に、タむマヌが終了する前にナヌザヌが蚀葉に答える時間がないずき、たたは最埌の瞬間に間違っお答えるずきに、「ゆるい鳥」効果が怜出されたした-圌は非垞に怒っおいたす。 感情は、この堎合はマむナスですが、プラスの効果がありたす。 ナヌザヌはレベルをクリアするたで萜ち着きたせん。぀たり、知らない単語を孊習したす。



配垃



䜕らかの理由で、アプリケヌションの配垃に密接に関䞎する機䌚がありたせんでした。 初日に行われた唯䞀のこず-リンクを゜ヌシャルに配眮したす。 VKontakteネットワヌク、および友人にアプリケヌションのダりンロヌドを求めるストヌリヌ。 初日は17回のダりンロヌドをもたらしたした。 その埌、ダりンロヌド数は1日に1に枛少したした。



8月20日、フォヌラムサむトw3bsit3-dns.comにトピックが䜜成され、アプリケヌションの説明ずPlayマヌケットぞのリンクが投皿されたした。 これたでに、700人以䞊がこのトピックを芋おきたした。 少し埌に、 r-android.ruサむトはうれしく驚き、圌のサむトでゲヌムのレビュヌを曞きたした。 最近では、ダりンロヌド数が30に増えおいたす。

ダりンロヌド統蚈
ダりンロヌド統蚈



w3bsit3-dns.comが倚くのナヌザヌを獲埗しおいるこずに泚意しおください 。 そしお、ほずんどの堎合、サむトr-android.ruの䜜成者はそこからゲヌムに぀いお正確に孊び、レビュヌを曞きたした。



おわりに



振り返っおみるず、倚くのこずが間違っおいる、たたは間違ったタむミングで行われおいるこずがわかりたす。 たずえば、アプリケヌションの抂念をよく考えお、蚭蚈開発の途䞭でデザむナヌの䜜業の半分をやり盎す必芁がないようにするこずができたす。



UXのテストは、アむデアをテストするために、䞻に迅速に組み立おられたコンセプトで行うこずができたはずでした。 しかし、これはリリヌスの盎前に行われたした。



時間がないため、Flurryには統蚈を远加したせんでしたが、アプリケヌションの䜿甚に関する倚くの有甚で必芁な情報が埗られるようになりたした。



たた、AdMobの広告を远加しお、それがもたらす収入ずもたらすかどうかを確認したかったのです。 しかし、再び十分な時間がありたせんでした。

私もリリヌスの準備ができおいたせんでしたプレスリリヌスも蚘事も曞かれおいたせんでした。 この問題にもっずアプロヌチしたい堎合は、アプリケヌションを眮く圓日に、テヌマフォヌラムでメッセヌゞを配垃したり、゜ヌシャルネットワヌクでアプリケヌションペヌゞを䜜成したり、さたざたなサむトに蚘事を送信したりできたす。 これはすべお、理論的には倚数のダりンロヌドをもたらす可胜性があり、Play Marketの評䟡におけるアプリケヌションの䜍眮にプラスの圱響を䞎えたす。



英語孊習のトピックは非垞に関連性が高いため、今は遞択肢がありたす小芏暡な曎新コンテンツの曎新、抂念的な曎新音声翻蚳などの新しいタむプのレベルの远加、たたは英語孊習甚のアプリケヌションパッケヌゞ時制、䞍芏則動詞の䜜成など。



圌らは、「最初のパンケヌキはゎツゎツしおいる」ず蚀いたす。 しかし、これはFunglishには圓おはたらないず思いたす。 このゲヌムの開発は非垞に貎重な䜓隓です。 このゲヌムには良い評䟡があり、人々はレビュヌを曞いお、いく぀かの新しい機胜を远加するよう求めおいたす。 誰かがあなたのアプリケヌションを本圓に気に入っおいるこずを知っおずおもうれしいです。 新しいゲヌムやアプリケヌションを䜜成するこずは心匷いです。



All Articles