前回はタッチ入力時に、マルチスレッドにより発生する問題について書きました。
今回はそれを解決する方法について書きます。
いきなりですが、問題を解決したソースコードです。
public class CActivityMain extends Activity implements Runnable, View.OnTouchListener { static public final class CTouchEvent { public int mAction; public float mX; public float mY; CTouchEvent(MotionEvent iEvent) { mAction = iEvent.getAction(); mX = iEvent.getX(); mY = iEvent.getY(); } } private List<CTouchEvent> mEventListMain = new ArrayList<CTouchEvent>(); private List<CTouchEvent> mEventListUI = new ArrayList<CTouchEvent>(); private SurfaceView mView; private float mX; private float mY; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mView = (SurfaceView)findViewById(R.id.surfaceView1); mView.setOnTouchListener(this); new Thread(this).start(); } @Override public void run() { SurfaceHolder aHolder = mView.getHolder(); Paint aPaint = new Paint(); aPaint.setColor(Color.RED); while(true) { if(aHolder.getSurface().isValid()) { synchronized (this) { mEventListMain.clear(); List<CTouchEvent> aTemporary = mEventListMain; mEventListMain = mEventListUI; mEventListUI = aTemporary; } for(CTouchEvent aTouchEvent : mEventListMain) { if(aTouchEvent.mAction == MotionEvent.ACTION_DOWN) { mX = aTouchEvent.mX; mY = aTouchEvent.mY; } } Canvas aCanvas = aHolder.lockCanvas(); aCanvas.drawColor(Color.WHITE); aCanvas.drawCircle(mX, mY, 50.0f, aPaint); aHolder.unlockCanvasAndPost(aCanvas); } } } @Override public boolean onTouch(View iView, MotionEvent iEvent) { synchronized (this) { mEventListUI.add(new CTouchEvent(iEvent)); } return true; } }
今回の話はこれでほとんど終わりなのですが、そういうわけにもいかないので少し解説します。
まずタッチイベント用のクラスとリストの部分です。
static public final class CTouchEvent { public int mAction; public float mX; public float mY; CTouchEvent(MotionEvent iEvent) { mAction = iEvent.getAction(); mX = iEvent.getX(); mY = iEvent.getY(); } } private List<CTouchEvent> mEventListMain = new ArrayList<CTouchEvent>(); private List<CTouchEvent> mEventListUI = new ArrayList<CTouchEvent>();
MotionEventの情報を保持するクラスと、そのリストです。
なぜ2つあるのかについては、後で書きます。
とりあえずタッチイベントを保持するリストが2つあると思ってください。
次はonTouch関数です。
public boolean onTouch(View iView, MotionEvent iEvent) { synchronized (this) { mEventListUI.add(new CTouchEvent(iEvent)); } return true; }
mEventListUIの方のリストにイベントをそのまま追加しています。
synchronizedで囲んでいるので、次に説明するrunの方のsynchronized部分とは同時に実行されません。
さて、そのrunです。
synchronized (this) { mEventListMain.clear(); List<CTouchEvent> aTemporary = mEventListMain; mEventListMain = mEventListUI; mEventListUI = aTemporary; } for(CTouchEvent aTouchEvent : mEventListMain) { if(aTouchEvent.mAction == MotionEvent.ACTION_DOWN) { mX = aTouchEvent.mX; mY = aTouchEvent.mY; } }
synchronized内で、mEventListMainをクリアして、mEventListMainとmEventListUIをswap(交換)しています。
結果mEventListUIの方が空になります。
そのあとsynchronizedの外でmEventListMainのイベントを処理しています。
ちょっと処理の流れが複雑なので、どんな感じになるかという例を書きます。
1.最初、mEventListMainとmEventListUIは空です。
2.タッチが発生し、onTouch()でmEventListUIにイベントが追加されます。
3.mEventListMain.clear()ですが、すでに空なのでなにもしません。
4.run()でmEventListMainとmEventListUIがswapされます。
(これで先ほどタッチされたイベントはmEventListMainに移行しました。
mEventListUIは空になります)
5.mEventListMain内のタッチイベントが処理され、mX、mYが更新されます。
6.再びタッチが発生し、onTouch()でmEventListUIにイベントが追加されます。
7.mEventListMain.clear()で2のタッチイベントがクリアされます。
8.4~5と同じ
つまり2つのリストのうちの一方は追加用、もう一方は処理用であり、runの最初の部分でその二つを入れ替えます。
mEventListUIにいくら追加されても、runのsynchronized部分にくるまで、イベントは処理されません。
ソースコードを見ての通り、synchronized内の処理は非常に軽量です。
またゲームの規模が大きくなったとしてもこの処理が増えることはありません。
とてもスマートに2つのスレッドでデータを受け渡すことができました。
このようなテクニックは「ダブルバッファリング」と呼ばれ、タッチ入力に限らずマルチスレッドではよく使われます。
以上でタッチ入力とマルチスレッドに関する話は終了です。
このサンプルはマルチタッチに対応していませんが、対応するにはCTouchEventを拡張するか、タッチID毎にリストを持つなりすることになります。
最後に蛇足ですが、run内のsynchronizedは厳密には
mEventListMain.clear(); List<CTouchEvent> aTemporary = mEventListMain; synchronized (this) { mEventListMain = mEventListUI; mEventListUI = aTemporary; }
で十分です。