ゲーム開発フレームワークの作成-Mechanic Framework

こんにちは、Habrの住民!

今日は、Android向けのゲームを便利に開発するためのMechanic Frameworkというフレームワークを作成します。



画像



必要なもの:









まず、プロジェクトを作成します。

ファイル-新規-その他-Androidアプリケーションプロジェクト



画像

[新しいAndroidアプリケーション]ウィンドウが表示されます。 任意の名前(たとえば、Mechanic)を入力し、パッケージに名前を付けて、アプリケーションの最小バージョンのAndroidとターゲットバージョンを選択し、[次へ]をクリックします。



画像

次へをクリックします。



画像

アイコンを選択します(Androidアイコンが気に入らない場合は、[クリップアート-何かを選択して選択する]をクリックするか、独自のアイコンを配置します)。



画像

次へをクリックします。



画像

アクティビティの名前(MyGameなど)を選択し、[完了]をクリックします。



.xmlビジュアル編集ウィンドウが開き、閉じます。

AndroidManifest.xmlを開き、必要に応じてカスタマイズします。



画像



可能な場合はゲームをメモリカードにインストールし、デバイスの内部メモリを汚染しないようにするには、manifestフィールドに書き込みます

android:installLocation="preferExternal"
      
      





デバッグのためにアプリケーションにアクセスできるようにするには、applicationフィールドに書き込みます

 android:debuggable="true"
      
      





アプリケーションをポートレートモードまたはランドスケープモード(この場合はランドスケープモード)に固定するには、アクティビティフィールドに次のように記述します。

 android:screenOrientation="landscape"
      
      





エミュレータ上のアプリケーションがキーボードアクションを処理するために、同じフィールドに書き込みます

 android:configChanges="keyboard|keyboardHidden|orientation"
      
      





Google Playからアプリケーションをダウンロードすると、アプリケーションがメモリカード/インターネットなどにアクセスする必要があることに気づきます。そのため、メモリカードを制御し、アイドル時に画面がロックされないようにするために、

  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
      
      





マニフェストは次のようになります

 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.frame" android:versionCode="1" android:versionName="1.0" android:installLocation="preferExternal"> <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="18" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" android:debuggable="true" > <activity android:name="com.frame.MyGame" android:screenOrientation="landscape" android:configChanges="keyboard|keyboardHidden|orientation" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WAKE_LOCK"/> </manifest>
      
      







マニフェストを閉じます



ここで、フレームワークフレームワークを作成する必要があります。これは、入力、グラフィックスのレンダリングなどを制御し、後ですべてのインターフェイスを実装するインターフェイスです。

画像



入る



com.mechanic.inputという新しいパッケージを作成します

このパッケージに入力インターフェイスを作成し、このフォームに追加します

 public interface Input { public static class MechanicKeyEvent { public static final int KEY_DOWN = 0, KEY_UP = 1; public int Type; public int KeyCode; public char KeyChar; } public static class MechanicTouchEvent { public static final int TOUCH_DOWN = 0, TOUCH_UP = 1, TOUCH_DRAGGED = 2; public int Type; public int X, Y; public int Pointer; } public boolean IsKeyPressed(int KeyCode); public boolean IsKeyPressed(char KeyChar); public boolean IsTouchDown(int pointer); public int GetTouchX(int pointer); public int GetTouchY(int pointer); public float GetAccelX(); public float GetAccelY(); public float GetAccelZ(); public List<MechanicTouchEvent> GetTouchEvents(); public List<MechanicKeyEvent> GetKeyEvents(); }
      
      





GetKeyDown-ブール値。キーコードを受け入れ、ボタンが押されるとtrueを返します

GetTouchDown-ブール値。画面が押されるとtrueを返し、この関数は画面を押した指の番号を取得します。 古いバージョンのAndroidはマルチタッチをサポートしていません。

GetTouchX-押されたキーのX座標を返します

GetTouchY-押されたキーのY座標を返します

両方の最後の機能は指の番号を受け入れ

GetAccelX、GetAccelY、GetAccelZ-加速度計の任意の座標で加速度を返します。 携帯電話を縦方向に垂直に保持すると、Y軸に沿った加速度は9.6 m / s2になり、X軸とZ軸に沿って0 m / s2になります。



MechanicKeyEventとMechanicTouchEventをご覧ください

最初のクラスには、主要なイベント情報が格納されます。 タイプは常にKEY_DOWNまたはKEY_UPのいずれかです。 KeyCodeとKeyCharは、それぞれ数値と文字型でキー値を保存します。

2番目のクラスでは、XとYは画面を押す指の座標です。ポインターは指の番号です。 TOUCH_DRAGGEDは、指を動かすことを意味します。



気を散らして、入力インターフェイスがどのように調整されるかについて話す価値があります。

加速度計、キーボード、および画面のタップの場合、責任があるのは入力を実装するクラスではなく、それぞれ加速度計、キーボード、およびタッチインターフェイスを実装するクラスです。 入力はこれらのクラスのインスタンスを保存するだけです。 デザインパターンに精通している場合、単純な「ファサード」パターンがこの方法で実装されていることを知っておく必要があります。



これらはインターフェースです

 public interface Accelerometer extends SensorEventListener { public float GetAccelX(); public float GetAccelY(); public float GetAccelZ(); }
      
      







 public interface Keyboard extends OnKeyListener { public boolean IsKeyPressed(int keyCode); public List<KeyEvent> GetKeyEvents(); }
      
      







 public interface Touch extends OnTouchListener { public boolean IsTouchDown(int pointer); public int GetTouchX(int pointer); public int GetTouchY(int pointer); public List<TouchEvent> GetTouchEvents(); }
      
      







Inputは単にメソッドを他のクラスにリダイレクトするだけであり、それらは正直に動作し、結果をレイアウトすると推測するのは簡単です。



ファイル



ファイルを操作する時間です。 Fileクラスが既に存在するため、インターフェイスはFileIOと呼ばれます。

新しいパッケージcom.mechanic.fileioとその中に新しいインターフェースを作成します

 public interface FileIO { public InputStream ReadAsset(String name) throws IOException; public InputStream ReadFile(String name) throws IOException; public OutputStream WriteFile(String name) throws IOException; }
      
      





通常、すべての画像、音声、その他のファイルはプロジェクトアセットフォルダーに保存します。 最初の関数は、アセットから指定された名前のファイルを開き、AssetsManagerでの不必要なトラブルを回避します。 最後の2つの機能は、たとえばレコードを保存するために必要です。 データを保存するとき、デバイスのストレージに情報を含むテキストファイルを書き込み、それを読み取ります。 念のため、「。mechanicsave」など、「file.txt」よりもオリジナルのファイル名を考えてみてください-これも可能です。





パッケージcom.mechanic.audioと新しいオーディオインターフェイスを作成します

 public interface Audio { public Music NewMusic(String name); public Sound NewSound(String name); }
      
      







サウンドの保存と再生には2つのオプションがあります。 最初のオプションは、サウンドをロードして再生するときの通常のオプションですが、ほとんどの場合、このアプローチはショットや爆発のような小さなサウンドに適しており、バックグラウンドミュージックのような大きなサウンドファイルにはサウンドを完全にロードする意味がありません。動的にサウンドを読み込んで再生します。 最初と2番目のオプションでは、サウンドと音楽のインターフェイスがそれぞれ責任を負います。 定義は次のとおりです

 public interface Sound { public void Play(float volume); public void Close(); }
      
      







 public interface Music extends OnCompletionListener { public void Close(); public boolean IsLooping(); public boolean IsPlaying(); public boolean IsStopped(); public void Play(); public void SetLooping(boolean loop); public void SetVolume(float volume); public void Stop(); }
      
      







グラフィックス



パッケージcom.mechanic.graphicsを作成します

グラフィックスインターフェイスは、主にグラフィックスを担当します。

ここに彼の定義があります

 public interface Graphics { public static enum ImageFormat { ARGB_8888, ARGB_4444, RGB_565 } public Image NewImage(String fileName); public void Clear(int color); public void DrawPixel(int x, int y, int color); public void DrawLine(int x, int y, int x2, int y2, int color); public void DrawRect(int x, int y, int width, int height, int color); public void DrawImage(Image image, int x, int y, int srcX, int srcY, int srcWidth, int srcHeight); public void DrawImage(Image image, int x, int y); public int GetWidth(); public int GetHeight(); }
      
      





ImageFormatは、画像のロード方法を簡単に選択できる列挙型です。 実際、特別なことは何もしませんが、形式を渡す場所の列挙には、不要なメソッドと不要な名前Configがたくさんあるので、そうします。

NewImageは新しい画像を返します。変数に保存して描画します

Draw ...という名前のメソッドはそれ自体を表しており、最初のDrawImageメソッドは画像の一部のみを描画し、2番目のメソッドは画像全体を描画します。

GetWidthとGetHeightは、画像を描く「キャンバス」のサイズを返します



別のインターフェイスがあります-写真用

 public interface Image { public int GetWidth(); public int GetHeight(); public ImageFormat GetFormat(); public void Dispose(); }
      
      





すべてが十分に雄弁です



一元化されたゲーム管理



パッケージcom.mechanic.gameを作成します

アプリケーション全体の操作をサポートする最後から2番目の重要なインターフェースがあります-ゲーム

 public interface Game { public Input GetInput(); public FileIO GetFileIO(); public Graphics GetGraphics(); public Audio GetAudio(); public void SetScreen(Screen screen); public Screen GetCurrentScreen(); public Screen GetStartScreen(); }
      
      





過去の章のテーマ-私たちはちょうどそこにインターフェイスを突き出します。

しかし、スクリーンとは何ですか?



気を取らせてください。 ほとんどすべてのゲームは、メインメニュー、設定メニュー、ハイスコア画面、すべてのレベルなど、いくつかの「状態」で構成されています。 など 少なくとも5つの状態をサポートすることで、コードの深byに浸ることができるのも不思議ではありません。 画面抽象クラスは私たちを救います

 public abstract class Screen { protected final Game game; public Screen(Game game) { this.game = game; } public abstract void Update(float deltaTime); public abstract void Present(float deltaTime); public abstract void Pause(); public abstract void Resume(); public abstract void Dispose(); }
      
      





各画面の子孫(MainMenuScreen、SettingsScreen)は、この「状態」を担当します。 いくつかの機能があります。

更新-更新

現在-グラフィックの表示(便宜上、導入されたもので、実際、この関数は前のものと同様に呼び出されます)

一時停止-ゲームが一時停止されるたびに呼び出されます(画面ブロック)

再開-一時停止後のゲームの継続

破棄-ダウンロードした画像など、すべてのリソースを解放します



2つの関数で渡されるdeltaTimeについて少し話す価値があります。

より洗練されたゲーム開発者は、ゲームの速度(たとえば、プレイヤーの動き)がデバイスの速度に直接依存する場合、つまり サイクルごとに変数xを1ずつ増やすと、ネットブックと巨大なRAMを搭載したコンピューターでゲームが同じように動作することはありません。



したがって、truバリアントは次のとおりです。

  @Override public void Update(float deltaTime) { x += 150 * deltaTime; }
      
      







truバリアントではありません:

  @Override public void Update(float deltaTime) { x += 150; }
      
      





基本エラーが1つあります-非常に多くの場合、xを1.0f * deltaTime増やしますが、0から1の非整数に整数を追加しても結果が得られないことに気付かない場合があります。



画面をどのように変更しますか? ゲームインターフェースに戻る

SetScreen関数はすべてに責任があります。 現在の画面と開始画面を取得する機能もあります。



このコレクション全体を実装する時が来ました!



入力して開始





Inputインターフェイスには、イベントのリストを返すGetKeyEventsおよびGetTouchEvents関数があります。つまり、イベントが発生すると、プログラムは多くのオブジェクトを作成し、ガベージコレクターがクリーンアップします。 Androidアプリケーションのブレーキがかかる主な理由は何ですか? そうです-ガベージコレクターが過負荷になっています! 何とか問題を制御する必要があります。 続行する前に、Poolクラスを作成し、「オブジェクトプーリング」を実装します。これは、Mario Tsechnerの優れた書籍「Programming Games for Android」で提案されています。



その意味は、ガベージコレクターがアプリケーションに干渉することを許可せず、必要なリソースを無駄にしないことです。

 public class Pool<T> { public interface PoolFactory<T> { public T Create(); } private final List<T> Objects; private final PoolFactory<T> Factory; private final int MaxSize; public Pool(PoolFactory<T> Factory, int MaxSize) { this.Factory = Factory; this.MaxSize = MaxSize; Objects = new ArrayList<T>(MaxSize); } public T NewObject() { T obj = null; if (Objects.size() == 0) obj = Factory.Create(); else obj = Objects.remove(Objects.size() - 1); return obj; } public void Free(T object) { if (Objects.size() < MaxSize) Objects.add(object); } }
      
      





プールプールオブジェクトがあるとします。 それが私たちの使い方です

  PoolFactory<MechanicTouchEvent> factory = new PoolFactory<MechanicTouchEvent>() { @Override public MechanicTouchEvent Create() { return new MechanicTouchEvent(); } }; TouchEventPool = new Pool<MechanicTouchEvent>(factory, 100);
      
      





プール宣言

 TouchEventPool.Free(event);
      
      





イベントをプールに保存する

 event = TouchEventPool.NewObject();
      
      





プールからイベントを取得します。 リストが空の場合、イベントを使用した後、次の呼び出しまでプールに戻すので、これは怖いことではありません。

とても良いことです!



メカニック加速度計

 package com.mechanic.input; import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorManager; public class MechanicAccelerometer implements Accelerometer { float accelX, accelY, accelZ; public MechanicAccelerometer(Context context) { SensorManager manager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); if(manager.getSensorList(Sensor.TYPE_ACCELEROMETER).size() > 0) { Sensor accelerometer = manager.getSensorList(Sensor.TYPE_ACCELEROMETER).get(0); manager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } @Override public void onSensorChanged(SensorEvent event) { accelX = event.values[0]; accelY = event.values[1]; accelZ = event.values[2]; } @Override public float GetAccelX() { return accelX; } @Override public float GetAccelY() { return accelY; } @Override public float GetAccelZ() { return accelZ; } }
      
      





加速度計に加えて、このクラスはSensorEventListenerも実装します。加速度計だけでなく、コンパス、懐中電灯、その他のおもちゃも制御する必要があります。 これまでのところ、加速度計のみを実行しています。

コンストラクターで、センサーマネージャーを取得し、加速度計へのアクセスがあるかどうかを確認します。 一般に、理論的には、1つではなく複数の加速度計(これはリストであり、1つのオブジェクトではありません)、実際には常に1つです。 加速度計の数が0より大きい場合、最初の加速度計を取得して登録し、このクラスをリスナーとして公開します。 onAccuracyChangedが必要です。センサーの精度が失われた場合、使用しません。 onSensorChangedは、加速度計の値が変化すると常に呼び出されます。ここでは、読み取り値を取得します。



MechanicTouch

 package com.mechanic.input; import java.util.ArrayList; import java.util.List; import com.mechanic.input.Input.MechanicTouchEvent; import com.mechanic.input.Pool.PoolFactory; import android.os.Build.VERSION; import android.view.MotionEvent; import android.view.View; public class MechanicTouch implements Touch { boolean EnableMultiTouch; final int MaxTouchers = 20; boolean[] IsTouched = new boolean[MaxTouchers]; int[] TouchX = new int[MaxTouchers]; int[] TouchY = new int[MaxTouchers]; Pool<MechanicTouchEvent> TouchEventPool; List<MechanicTouchEvent> TouchEvents = new ArrayList<MechanicTouchEvent>(); List<MechanicTouchEvent> TouchEventsBuffer = new ArrayList<MechanicTouchEvent>(); float ScaleX; float ScaleY; public MechanicTouch(View view, float scaleX, float scaleY) { if(Integer.parseInt(VERSION.SDK) < 5) EnableMultiTouch = false; else EnableMultiTouch = true; PoolFactory<MechanicTouchEvent> factory = new PoolFactory<MechanicTouchEvent>() { @Override public MechanicTouchEvent Create() { return new MechanicTouchEvent(); } }; TouchEventPool = new Pool<MechanicTouchEvent>(factory, 100); view.setOnTouchListener(this); this.ScaleX = scaleX; this.ScaleY = scaleY; } @Override public boolean onTouch(View v, MotionEvent event) { synchronized (this) { int action = event.getAction() & MotionEvent.ACTION_MASK; @SuppressWarnings("deprecation") int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT; int pointerId = event.getPointerId(pointerIndex); MechanicTouchEvent TouchEvent; switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: TouchEvent = TouchEventPool.NewObject(); TouchEvent.Type = MechanicTouchEvent.TOUCH_DOWN; TouchEvent.Pointer = pointerId; TouchEvent.X = TouchX[pointerId] = (int)(event.getX(pointerIndex) * ScaleX); TouchEvent.Y = TouchY[pointerId] = (int)(event.getY(pointerIndex) * ScaleY); IsTouched[pointerId] = true; TouchEventsBuffer.add(TouchEvent); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_CANCEL: TouchEvent = TouchEventPool.NewObject(); TouchEvent.Type = MechanicTouchEvent.TOUCH_UP; TouchEvent.Pointer = pointerId; TouchEvent.X = TouchX[pointerId] = (int)(event.getX(pointerIndex) * ScaleX); TouchEvent.Y = TouchY[pointerId] = (int)(event.getY(pointerIndex) * ScaleY); IsTouched[pointerId] = false; TouchEventsBuffer.add(TouchEvent); break; case MotionEvent.ACTION_MOVE: int pointerCount = event.getPointerCount(); for (int i = 0; i < pointerCount; i++) { pointerIndex = i; pointerId = event.getPointerId(pointerIndex); TouchEvent = TouchEventPool.NewObject(); TouchEvent.Type = MechanicTouchEvent.TOUCH_DRAGGED; TouchEvent.Pointer = pointerId; TouchEvent.X = TouchX[pointerId] = (int)(event.getX(pointerIndex) * ScaleX); TouchEvent.Y = TouchY[pointerId] = (int)(event.getY(pointerIndex) * ScaleY); TouchEventsBuffer.add(TouchEvent); } break; } return true; } } @Override public boolean IsTouchDown(int pointer) { synchronized(this) { if(pointer < 0 || pointer >= MaxTouchers) return false; else return IsTouched[pointer]; } } @Override public int GetTouchX(int pointer) { synchronized(this) { if (pointer < 0 || pointer >= MaxTouchers) return 0; else return TouchX[pointer]; } } @Override public int GetTouchY(int pointer) { synchronized(this) { if (pointer < 0 || pointer >= 20) return 0; else return TouchY[pointer]; } } @Override public List<MechanicTouchEvent> GetTouchEvents() { synchronized (this) { for (int i = 0; i < TouchEvents.size(); i++) TouchEventPool.Free(TouchEvents.get(i)); TouchEvents.clear(); TouchEvents.addAll(TouchEventsBuffer); TouchEventsBuffer.clear(); return TouchEvents; } } }
      
      





Touchに加えて、OnTouchListenerも実装します

デバイスが複数の指の押下をサポートしているかどうかを判断するには、EnableMultiTouchが必要です。 VERSION.SDKが5未満の場合(何らかの理由でこの変数は文字列として表示されます)、サポートされません。

MaxTouchers-指の​​最大数。 それらの20があります、多かれ少なかれ。

onTouch関数では、指の番号とアクション(押す、持ち上げる、移動)を取得します。これらをイベントに記録し、イベントをリストに追加します。

GetTouchEventsでは、イベントのリストを返します。イベントのリストはその後クリアします。 別のリストがイベントのリストを返します。

あなたは、ScaleXとScaleYは何に責任があるのでしょうか? これについては、グラフィックセクションで後ほど説明します。



メカニックキーボード

 package com.mechanic.input; import java.util.ArrayList; import java.util.List; import android.view.KeyEvent; import android.view.View; import com.mechanic.input.Input.MechanicKeyEvent; import com.mechanic.input.Pool.PoolFactory; import com.mechanic.input.Pool; public class MechanicKeyboard implements Keyboard { boolean[] PressedKeys = new boolean[128]; Pool<MechanicKeyEvent> KeyEventPool; List<MechanicKeyEvent> KeyEventsBuffer = new ArrayList<MechanicKeyEvent>(); List<MechanicKeyEvent> KeyEvents = new ArrayList<MechanicKeyEvent>(); public MechanicKeyboard(View view) { PoolFactory<MechanicKeyEvent> pool = new PoolFactory<MechanicKeyEvent>() { @Override public MechanicKeyEvent Create() { return new MechanicKeyEvent(); } }; KeyEventPool = new Pool<MechanicKeyEvent>(pool,100); view.setOnKeyListener(this); view.setFocusableInTouchMode(true); view.requestFocus(); } public boolean IsKeyPressed(int KeyCode) { if(KeyCode < 0 || KeyCode > 127) return false; return PressedKeys[KeyCode]; } public List<MechanicKeyEvent> GetKeyEvents() { synchronized(this) { for(int i = 0; i < KeyEvents.size(); i++) KeyEventPool.Free(KeyEvents.get(i)); KeyEvents.clear(); KeyEvents.addAll(KeyEventsBuffer); KeyEventsBuffer.clear(); return KeyEvents; } } @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if(event.getAction() == KeyEvent.ACTION_MULTIPLE) return false; synchronized(this) { MechanicKeyEvent key = KeyEventPool.NewObject(); key.KeyCode = keyCode; key.KeyChar = (char)event.getUnicodeChar(); if(event.getAction() == KeyEvent.ACTION_DOWN) { key.Type = MechanicKeyEvent.KEY_DOWN; if(keyCode > 0 && keyCode < 128) PressedKeys[keyCode] = true; } if(event.getAction() == KeyEvent.ACTION_UP) { key.Type = MechanicKeyEvent.KEY_UP; if(keyCode > 0 && keyCode < 128) PressedKeys[keyCode] = false; } KeyEventsBuffer.add(key); } return false; } }
      
      





128個のブール変数の配列を作成し、128個のキーが押されたかどうかに関する情報を保持します。 また、オブジェクトのプールと2つのリストを作成します。 すべてがシンプルです



MechanicInput

 package com.mechanic.input; import java.util.List; import android.content.Context; import android.view.View; public class MechanicInput implements Input { MechanicKeyboard keyboard; MechanicAccelerometer accel; MechanicTouch touch; public MechanicInput(Context context, View view, float scaleX, float scaleY) { accel = new MechanicAccelerometer(context); keyboard = new MechanicKeyboard(view); touch = new MechanicTouch(view, scaleX, scaleY); } @Override public boolean IsKeyPressed(int keyCode) { return keyboard.IsKeyPressed(keyCode); } @Override public boolean IsKeyPressed(char keyChar) { return keyboard.IsKeyPressed(keyChar); } @Override public boolean IsTouchDown(int pointer) { return touch.IsTouchDown(pointer); } @Override public int GetTouchX(int pointer) { return touch.GetTouchX(pointer); } @Override public int GetTouchY(int pointer) { return touch.GetTouchY(pointer); } @Override public float GetAccelX() { return accel.GetAccelX(); } @Override public float GetAccelY() { return accel.GetAccelY(); } @Override public float GetAccelZ() { return accel.GetAccelZ(); } @Override public List<MechanicTouchEvent> GetTouchEvents() { return touch.GetTouchEvents(); } @Override public List<MechanicKeyEvent> GetKeyEvents() { return keyboard.GetKeyEvents(); } }
      
      





「ファサード」パターンを実現します。



これで、ファイルを操作する時間です!



ファイルを操作する





MechanicFileIO

 package com.mechanic.fileio; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import android.content.res.AssetManager; import android.os.Environment; public class MechanicFileIO implements FileIO { AssetManager assets; String ExternalStoragePath; public MechanicFileIO(AssetManager assets) { this.assets = assets; ExternalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator; } public InputStream ReadAsset(String name) throws IOException { return assets.open(name); } public InputStream ReadFile(String name) throws IOException { return new FileInputStream(ExternalStoragePath + name); } public OutputStream WriteFile(String name) throws IOException { return new FileOutputStream(ExternalStoragePath + name); } }
      
      





アセットマネージャーからアセットフォルダーからファイルを削除します。最初の関数はそれを使用し、2番目の2つの関数はAndroidの特別なデバイスフォルダーからファイルを取得します。 コンストラクターでこのフォルダーへのパスを取得します。



サウンドを作成します

音を扱う





MechanicSound

 package com.mechanic.audio; import android.media.SoundPool; public class MechanicSound implements Sound { int id; SoundPool pool; public MechanicSound(SoundPool pool, int id) { this.pool = pool; this.id = id; } public void Play(float volume) { pool.play(id, volume, volume, 0, 0, 1); } public void Close() { pool.unload(id); } }
      
      







MechanicAudioでは、SoundPoolを使用して小さな効果音を保持します。 MechanicSoundでは、サウンドエフェクト番号とSoundPoolオブジェクト自体を渡し、そこからサウンドを生成します



MechanicMusic

 package com.mechanic.audio; import java.io.IOException; import android.content.res.AssetFileDescriptor; import android.media.MediaPlayer; public class MechanicMusic implements Music { MediaPlayer Player; boolean IsPrepared = false; public MechanicMusic(AssetFileDescriptor descriptor) { Player = new MediaPlayer(); try { Player.setDataSource(descriptor.getFileDescriptor(), descriptor.getStartOffset(), descriptor.getLength()); Player.prepare(); IsPrepared = true; } catch(Exception ex) { throw new RuntimeException("   "); } } public void Close() { if(Player.isPlaying()) Player.stop(); Player.release(); } public boolean IsLooping() { return Player.isLooping(); } public boolean IsPlaying() { return Player.isPlaying(); } public boolean IsStopped() { return !IsPrepared; } public void Play() { if(Player.isPlaying()) return; try { synchronized(this) { if(!IsPrepared) Player.prepare(); Player.start(); } } catch(IllegalStateException ex) { ex.printStackTrace(); } catch(IOException ex) { ex.printStackTrace(); } } public void SetLooping(boolean loop) { Player.setLooping(loop); } public void SetVolume(float volume) { Player.setVolume(volume, volume); } public void Stop() { Player.stop(); synchronized(this) { IsPrepared = false; } } @Override public void onCompletion(MediaPlayer player) { synchronized(this) { IsPrepared = false; } } }
      
      





サウンドファイルをストリームに配置して再生します。

IsPreparedは、サウンドが作業の準備ができているかどうかを示します。

このクラスを理解することをお勧めします。



MechanicAudioに行きました

 package com.mechanic.audio; import java.io.IOException; import android.app.Activity; import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import android.media.AudioManager; import android.media.SoundPool; public class MechanicAudio implements Audio { AssetManager assets; SoundPool pool; public MechanicAudio(Activity activity) { activity.setVolumeControlStream(AudioManager.STREAM_MUSIC); this.assets = activity.getAssets(); pool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0); } public Music NewMusic(String name) { try { AssetFileDescriptor descriptor = assets.openFd(name); return new MechanicMusic(descriptor); } catch(IOException ex) { throw new RuntimeException("    " + name); } } public Sound NewSound(String name) { try { AssetFileDescriptor descriptor = assets.openFd(name); int id = pool.load(descriptor, 0); return new MechanicSound(pool, id); } catch(IOException ex) { throw new RuntimeException("    " + name); } } }
      
      





コンストラクターでは、デバイスごとに音楽を調整し、アセットマネージャーを使用して、一度に20以下のサウンドエフェクトしか再生できないSoundPoolを作成できます。ほとんどのゲームではこれで十分だと思います。

Musicの作成では、ファイル記述子をMechanicMusicコンストラクターに渡します。Soundでは、サウンドをsoundPoolに読み込み、プールとサウンド番号をMechanicSoundコンストラクターに渡します。何か問題が発生した場合、例外が作成されます。



製図工を作ります



グラフィックを使用する





MechanicImage

 package com.mechanic.graphics; import com.mechanic.graphics.Graphics.ImageFormat; import android.graphics.Bitmap; public class MechanicImage implements Image { Bitmap bitmap; ImageFormat format; public MechanicImage(Bitmap bitmap, ImageFormat format) { this.bitmap = bitmap; this.format = format; } @Override public int GetWidth() { return bitmap.getWidth(); } @Override public int GetHeight() { return bitmap.getHeight(); } @Override public ImageFormat GetFormat() { return format; } @Override public void Dispose() { bitmap.recycle(); } }
      
      





このクラスはイメージホルダーです。彼は特別なことは何もせず、便宜上導入されました。



MechanicGraphics

 package com.mechanic.graphics; import java.io.IOException; import java.io.InputStream; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Rect; public class MechanicGraphics implements Graphics { AssetManager assets; Bitmap buffer; Canvas canvas; Paint paint; Rect srcRect = new Rect(), dstRect = new Rect(); public MechanicGraphics(AssetManager assets, Bitmap buffer) { this.assets = assets; this.buffer = buffer; this.canvas = new Canvas(buffer); this.paint = new Paint(); } @Override public Image NewImage(String fileName) { ImageFormat format; InputStream file = null; Bitmap bitmap = null; try { file = assets.open(fileName); bitmap = BitmapFactory.decodeStream(file); if (bitmap == null) throw new RuntimeException("   '" + fileName + "'"); } catch (IOException e) { throw new RuntimeException("   '" + fileName + "'"); } finally { try { if(file != null) file.close(); } catch(IOException e) { } } if (bitmap.getConfig() == Config.RGB_565) format = ImageFormat.RGB_565; else if (bitmap.getConfig() == Config.ARGB_4444) format = ImageFormat.ARGB_4444; else format = ImageFormat.ARGB_8888; return new MechanicImage(bitmap, format); } @Override public void Clear(int color) { canvas.drawRGB((color & 0xff0000) >> 16, (color & 0xff00) >> 8, (color & 0xff)); } @Override public void DrawPixel(int x, int y, int color) { paint.setColor(color); canvas.drawPoint(x, y, paint); } @Override public void DrawLine(int x, int y, int x2, int y2, int color) { paint.setColor(color); canvas.drawLine(x, y, x2, y2, paint); } @Override public void DrawRect(int x, int y, int width, int height, int color) { paint.setColor(color); paint.setStyle(Style.FILL); canvas.drawRect(x, y, x + width - 1, y + width - 1, paint); } @Override public void DrawImage(Image image, int x, int y, int srcX, int srcY, int srcWidth, int srcHeight) { srcRect.left = srcX; srcRect.top = srcY; srcRect.right = srcX + srcWidth - 1; srcRect.bottom = srcY + srcHeight - 1; dstRect.left = x; dstRect.top = y; dstRect.right = x + srcWidth - 1; dstRect.bottom = y + srcHeight - 1; canvas.drawBitmap(((MechanicImage)image).bitmap, srcRect, dstRect, null); } @Override public void DrawImage(Image image, int x, int y) { canvas.drawBitmap(((MechanicImage)image).bitmap, x, y, null); } @Override public int GetWidth() { return buffer.getWidth(); } @Override public int GetHeight() { return buffer.getHeight(); } }
      
      





注意してください!これはガベージコレクターに対する犯罪であるため、描画するたびにPaintおよびRectオブジェクトを作成するわけではありません。

コンストラクターでは、Bitmapを使用します。これは、すべてを描画するバッファーであり、キャンバスによって使用されます。

画像を読み込むと、アセットから画像を読み取り、ビットマップでデコードします。ダウンロードしたファイルが画像でない場合、またはファイルが存在しない場合は、例外がスローされ、ファイルは閉じられます。最後に、イメージ形式を取得して新しいMechanicImageを返し、BitmapとImageFormatをコンストラクターに渡します。また、画像の一部を描画する最初のDrawImageメソッドも注目に値します。これは、アトラスと呼ばれる画像のグループがゲーム内の個々の画像の代わりに使用される場合に適用されます。このようなアトラスの例は

画像

次のとおりです(interesnoe.info Webリソースからの画像)

位置1.1で32.32から48.48の画像の一部を描画する必要があるとします。それからそうする

 DrawImage(image, 1, 1, 32, 32, 16, 16);
      
      





残りの方法は簡単に理解でき、興味の対象ではありません。



ゲームと画面のインターフェイスの時間です!



続行する前に、グラフィックを別のストリームに描画し、ユーザーストリームを読み込まないようにする必要があります。

別のスレッドでグラフィックスを描画できるSurfaceViewクラスをご覧ください。ランナークラスを作成する

 package com.mechanic.game; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.view.SurfaceHolder; import android.view.SurfaceView; public class Runner extends SurfaceView implements Runnable { MechanicGame game; Canvas canvas; Bitmap buffer; Thread thread = null; SurfaceHolder holder; volatile boolean running = false; public Runner(Object context, MechanicGame game, Bitmap buffer) { super(game); this.game = game; this.buffer = buffer; this.holder = getHolder(); } public void Resume() { running = true; thread = new Thread(this); thread.start(); } public void run() { Rect dstRect = new Rect(); long startTime = System.nanoTime(); while(running) { if(!holder.getSurface().isValid()) continue; float deltaTime = (System.nanoTime()-startTime) / 1000000000.0f; startTime = System.nanoTime(); game.GetCurrentScreen().Update(deltaTime); game.GetCurrentScreen().Present(deltaTime); canvas = holder.lockCanvas(); canvas.getClipBounds(dstRect); canvas.drawBitmap(buffer, null, dstRect, null); holder.unlockCanvasAndPost(canvas); } } public void Pause() { running = false; while(true) { try { thread.join(); break; } catch (InterruptedException e) { } } } }
      
      





MechanicGameクラスは近日公開予定です。心配しないでください。

ユーザーインターフェイスにないグラフィックを描画するには、SurfaceHolderオブジェクトが必要です。主な機能は、lockCanvasとunlockCanvasAndPostです。最初の関数はSurfaceをブロックし、何かを描画するCanvas(この場合、キャンバスとして機能するBitmapバッファー)を返します。

Resume関数では、このクラスで新しいスレッドを開始します。

run関数では、アプリケーションの実行中に、最後のサイクルからの経過時間が取得され(System.nanoTimeはナノ秒を返します)、アプリケーションの現在の画面の更新および現在の関数が呼び出され、その後バッファーが描画されます。



これがMechanicGameクラスです

 package com.mechanic.game; import com.mechanic.audio.Audio; import com.mechanic.audio.MechanicAudio; import com.mechanic.fileio.FileIO; import com.mechanic.fileio.MechanicFileIO; import com.mechanic.graphics.Graphics; import com.mechanic.graphics.MechanicGraphics; import com.mechanic.input.Input; import com.mechanic.input.MechanicInput; import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.os.Bundle; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.view.Window; import android.view.WindowManager; public abstract class MechanicGame extends Activity implements Game { Runner runner; Graphics graphics; Audio audio; Input input; FileIO fileIO; Screen screen; WakeLock wakeLock; static final int SCREEN_WIDTH = 80; static final int SCREEN_HEIGHT = 128; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); boolean IsLandscape = (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); int frameBufferWidth = IsLandscape ? SCREEN_HEIGHT : SCREEN_WIDTH; int frameBufferHeight = IsLandscape ? SCREEN_WIDTH : SCREEN_HEIGHT; Bitmap frameBuffer = Bitmap.createBitmap(frameBufferWidth, frameBufferHeight, Config.RGB_565); float scaleX = (float) frameBufferWidth / getWindowManager().getDefaultDisplay().getWidth(); float scaleY = (float) frameBufferHeight / getWindowManager().getDefaultDisplay().getHeight(); runner = new Runner(null, this, frameBuffer); graphics = new MechanicGraphics(getAssets(), frameBuffer); fileIO = new MechanicFileIO(getAssets()); audio = new MechanicAudio(this); input = new MechanicInput(this, runner, scaleX, scaleY); screen = GetStartScreen(); setContentView(runner); PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "Game"); } @Override public Input GetInput() { return input; } @Override public FileIO GetFileIO() { return fileIO; } @Override public Graphics GetGraphics() { return graphics; } @Override public Audio GetAudio() { return audio; } @Override public void SetScreen(Screen screen) { if (screen == null) throw new IllegalArgumentException("Screen    null"); this.screen.Pause(); this.screen.Dispose(); screen.Resume(); screen.Update(0); this.screen = screen; } @Override public Screen GetCurrentScreen() { return screen; } @Override public Screen GetStartScreen() { return null; } @Override public void onResume() { super.onResume(); wakeLock.acquire(); screen.Resume(); runner.Resume(); } @Override public void onPause() { super.onPause(); wakeLock.release(); runner.Pause(); screen.Pause(); if(isFinishing()) screen.Dispose(); } }
      
      







このクラスには、Runnerオブジェクト、すべてのインターフェイスとクラス、およびWakeLockオブジェクト(ゲームの実行中に携帯電話がスリープ状態にならないようにするために必要)



があり、2つの定数があります。

デバイスには多くの解像度があり、各デバイスの画像サイズの調整、場所の計算などはほとんど不可能で無意味です。など(上記の2つの定数から)80x128ピクセルを測定するウィンドウがあると想像してください。このウィンドウに小さな絵を描きます。しかし、突然、デバイスの画面サイズがこのウィンドウのサイズに適合しません。どうするすべてが非常に簡単です。ウィンドウの幅と長さとデバイスの幅と長さの比を取り、この比が与えられるとすべての絵を描きます。

その結果、アプリケーション自体がデバイスの画面下で画像を拡大します。



このクラスにはActivityが含まれ、onCreate、onResume、およびonPauseのメソッドがあります。

onCreateでは、アプリケーションは最初にフルスクリーンモードになります(そのため、上部に充電と時間が表示されなくなります)。次に、電話の向き(横向きまたは縦向き)が判明します(記事の冒頭で.xmlファイルに既に記述されています)。次に、このウィンドウのサイズが80x128ピクセルの待望のバッファーが作成されます。このウィンドウとデバイスのサイズの比率は、MechanicInputコンストラクターに渡され、それがリレーションをMechanicTouchに渡します。そしてここ-ビンゴ!結果の画面上のタッチポイントはこの比率で乗算されるため、クリックの座標はデバイスのサイズに依存しません。

次に、インターフェイスを作成し、RunnerとWakeLockを登録します。

SetScreenメソッドでは、現在のScreenを解放し、別のScreenを作成します。

興味のある他の方法は提供しません。



それだけですか?



はい、紳士、フレームワークは準備ができています!

終わったら。



そして、たとえばMyGameなどのメインクラスにフレームワークを接続する方法を教えてください。



「メイン」クラスは次のようになります

 public class MyGame extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_my_game); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.my_game, menu); return true; } }
      
      





このクラスに変更します

 package com.mechanic; import com.mechanic.game.MechanicGame; import com.mechanic.game.Screen; public class MyGame extends MechanicGame { @Override public Screen GetStartScreen() { return new GameScreen(this); } }
      
      





MechanicGame自体がActivityから継承するため、JavaはこのクラスをActivityからの継承者として認識します。onCreateはすでに登録されています。このメソッドはMechanicGameでnullを返し、エラーをスローするため、GetStartScreen()をオーバーライドするだけです。

GameScreenクラスを実装することを忘れないでください:)

 package com.mechanic; import com.mechanic.game.Game; import com.mechanic.game.Screen; import com.mechanic.graphics.Graphics; import com.mechanic.graphics.Image; public class GameScreen extends Screen { Graphics g = game.GetGraphics(); Image wikitan; float x = 0.0f; public GameScreen(Game game) { super(game); wikitan = g.NewImage("wikipetan.png"); } @Override public void Update(float deltaTime) { if(game.GetInput().IsTouchDown(0)) x += 1.0f * deltaTime; } @Override public void Present(float deltaTime) { g.Clear(0); g.DrawImage(wikitan, (int)x, 0); } @Override public void Pause() { } @Override public void Resume() { } @Override public void Dispose() { wikitan.Dispose(); } }
      
      







これは、Wikipe-tanイメージをダウンロードし、画面をクリックして移動するScreen実装の簡単な例です。

画像

(ru.wikipedia.org Webリソースからの画像)



結果

画像



変数xは、0から1の数値を追加しても何も得られないため、浮動小数点数として表されます。

キャンバスのサイズは80x128ピクセルなので、Wikipe-tanは増加して描画されます



質問と回答:



-画像が正しく描画されません-90度回転しました!

-これは、xmlファイルでコマンドを横モードでのみ動作するように指定したためです。モードを切り替えるには、キーボードの右側にあるキー7を押します

-正直にx + = 1.0f * deltaTimeを変更しますが、画像が動かないか、ゆっくり動きます。 どうする

-エミュレーターは非常に遅いものです。デバイス上のアプリケーションの状態を確認します。



楽しんでください!



出典:

rghost.ru/49052713

github.com/Izaron/MechanicFramework

文献:

developer.alexanderklimov.ru/android

habrahabr.ru/post/109944

マリオ・テシュナーの「Android向けゲームのプログラミング」






All Articles