After a lot of tinkering, I think I've finally come up with a good multitouch handling system for my android game. It makes use of Robert Greene's Input Pipeline, modified for use with multitouch. Right now, the code has a simple system that records which pointer ID is currently doing which action (right now just shooting and moving). Each pointer's state is kept in a Pointer class, which is just a simple encapsulation of whether it is down, and it's coordinates. The ID acts as the pointer array index.
This seems like it should work well in practice, but in game it behaves very erratic. When recording the pointer actions in LogCat, oftentimes Android will send an "UP" action when the pointer remains down, or just before a number of "MOVE" actions by the pointer. Because my code believes the pointer is up, the game doesn't respond to it.
This also happens with button presses like the shooting button. When the pointer comes down on the area (which right now is just simply the lower left region), Android will send multiple "UP" and "DOWN" actions even though the pointer remains down the whole time. I had a single touch movement system before and none of these problems happened.
Is this just an issue with how I am reacting to the events? Should I handle POINTER_DOWN and DOWN separately? Or should I detect which pointers are moving after the "UP" action to see which ones really are down despite what Android says?
Here's my current code in my thread which receives the input events from Android. Because it is a pipeline system, I have the events encapsulated in the InputObject which is somewhat similar to Robert Greene's. Maybe a new set of eyes can help me tell what's wrong? Thanks for any help!
private int inx, iny;
private int shootID;
public boolean shooting = false;
private int moveID;
public boolean moveDown = false;
private static final int MAX_POINTERS = 10;
private Pointer[] pointers = new Pointer[MAX_POINTERS];
public void inputTouch(InputObject in) {
switch(in.action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
pointers[in.pID].press(in.pX[in.actionIndex], in.pY[in.actionIndex]);
//Log.i("D", "DOWN");
break;
case MotionEvent.ACTION_MOVE:
for(int p = 0; p < in.pointCount; p++) {
int id = in.pointerIDs[p];
pointers[id].setCoord(in.pX[id], in.pY[id]);
}
//Log.i("D", "MOVE");
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
pointers[in.pID].release();
if(shootID == in.pID) {
shooting = false;
}
if(moveID == in.pID) {
moveDown = false;
}
//Log.i("D", "UP");
break;
case MotionEvent.ACTION_CANCEL:
default:
break;
}
for(int ap = 0; ap < MAX_POINTERS; ap++) {
if(pointers[ap].down) {
if(pointers[ap].x < world.cam.pixelWidth / 4 &&
pointers[ap].y > world.cam.pixelHeight - (world.cam.pixelHeight / 4)) {
shootID = ap;
shooting = true;
} else {
inx = pointers[ap].x;
iny = pointers[ap].y;
moveID = ap;
moveDown = true;
}
}
}
StringBuilder sb = new StringBuilder();
for(int j = 0; j < 3; j++) {
sb.append("ID " + (j+1) + ": " + pointers[j].down + "[" + pointers[j].x + ", " + pointers[j].y + "]" + " | ");
}
//Log.i("D", sb.toString());
}
Hope you got the answer by now, but here''s my solution:
First of all, there's a couple of things to add in the InputHolder class:
-internal public fields for mPointerIndex = event.getPointerIndex and mPointerID = event.getPointerID(mPointerIndex) (these get assigned in the useEvent/useHistory)
-if you only need to track 2 touchpoints, you need to add mPointerIndex2, x2 and y2 aswell. Add more as you need to track more points.
-add definitions for the case where MotionEvent.ACTION_POINTER_2_UP/DOWN gets passed. Turns out, POINTER_1 is the same as POINTER_UP/DOWN! This really tripped me up because I was falling through the switch case to the default. I only caught this because I changed my default to -1 and saw it logged.
Depending on how you process the actions in your processInput(obj), you map these to different ints. In my case I used the obj.y to see if they where left/right touches and I only needed two points, so I mapped these to ACTION_TOUCH_POINTER_UP/DOWN instead of giving each touch it's own action int identifier.
-now, if you want to track multiple touch points, you would have to do the above and the below in a for loop over all entries in event.getPointerCount(). In my case I was only interested in the x/y of one other touchpoint, so I could get away with only doing a check after I had filled the first point and the other pointerindex was easy to deduce:
public void useEvent(MotionEvent event) {
eventType = EVENT_TYPE_TOUCH;
int a = event.getAction();
switch (a) {
case MotionEvent.ACTION_DOWN:
action = ACTION_TOUCH_DOWN;
break;
case MotionEvent.ACTION_POINTER_DOWN:
action = ACTION_TOUCH_POINTER_DOWN;
break;
case MotionEvent.ACTION_POINTER_2_DOWN:
action = ACTION_TOUCH_POINTER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
action = ACTION_TOUCH_MOVE;
break;
case MotionEvent.ACTION_UP:
action = ACTION_TOUCH_UP;
break;
case MotionEvent.ACTION_POINTER_UP:
action = ACTION_TOUCH_POINTER_UP;
break;
case MotionEvent.ACTION_POINTER_2_UP:
action = ACTION_TOUCH_POINTER_UP;
break;
default:
action = -1;
}
time = event.getEventTime();
pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;
pointerID = event.getPointerId(pointerIndex);
x = (int) event.getX(pointerIndex);
y = (int) event.getY(pointerIndex);
if (event.getPointerCount() > 1)
{
pointerIndex2 = pointerIndex== 0 ? 1 : 0;
x2 = (int)event.getX(pointerIndex2);
y2 = (int)event.getY(pointerIndex2);
}
}
If you''re tracking more points, or need more information about each touchevent, you have to extend the for loop up over the action switch case.
Anyway, now you need two variables in your thread so you can keep track of your two touches: call 'em touchOneID and touchTwoID (or index). On an ACTION_POINTER_2_UP, the obj.mPointerID refers to the obj which is going UP! This means that the other touch will change it's ID! Keep track of this change, and you're sorted. The internal obj's mPointerID/Index will always be correct, you just have to track them correctly in your surfaceview's thread so you can act accordingly to when you get a POINTER_DOWN. In my case, I did a simple x position check to determine what to do in my ACTION_MOVE event which told me enough to correctly determine which x/y or x2/y2 I should use and how to use it. This meant less code to run and limits to the things I had to keep in memory, but it all depends on what and how much information you need.
Finally:
To be honest, if you handled the definitions correctly and assigned every MotionEvent and held them in the InputObject, you'd probably be fine. Hell, I think you can ignore and lose the whole switch case and just say obj.mAction = event.getAction() and handle these in your processInput(obj)!
Meaning, all those static ints he's remapping to in the InputHolder seem unnecessary, unless you really only need one or two touch definitions (which explains his mysterious standalone comment of "this app is only interested in down touches"). Getting rid of those statically defined ints also means you can just test against MotionEvent.ACTION_CODE instead of doing a lookup against InputHolder.TOUCH_ACTION_CODE.
Related
I am having a hard time trying to ensure no duplicates numbers appear in the following code below. I tried to create an ArrayList to add all the possible index of the array and then gradually remove the values of the array from the List as more elements are generated by the number generator. This; however, didnt work as I expected. I've been trying to think about it but Haven't been able to come up with a viable solution.
Random randGen = new Random();
public boolean onTouchEvent(MotionEvent touchevent) {
switch (touchevent.getAction()) {
case MotionEvent.ACTION_DOWN: {
x1 = touchevent.getX();
y1 = touchevent.getY();
break;
}
case MotionEvent.ACTION_UP: {
x2 = touchevent.getX();
y2 = touchevent.getY();
int maxLength=exampleArray.length;
List<Integer>indices=new ArrayList<Integer>(maxLength);
for(int i=0;i<maxLength;i++){
indices.add(i);
}
int randomApple = randGen.nextInt(exampleArray.length);
int randomNum=indices.get(randomApple) ;
indices.remove(randomApple);
textView.setText(exampleArray[randomNum]);
if (x1 < x2) {
}
if (x1 > x2) {
//textView.setText(exampleArray[randomNum]);
}
}
}
return false;
}
I think you are missing some steps and adding extraneous ones....
One simple way (if you don't care whether the numbers keep going up) is to keep adding a random positive value to a counter. That way the new counter will always be different.
If you do:
Another way to do this is to creating a list of numbers beforehand (either sequentially [1, 2, 3...] or you randomly add some amount to each new index [rnd1, rnd1 + rnd2, rnd1 + rnd2 + rnd3...]), randomizing the ordering by shuffling the numbers (click this!), and then simply iterating through the created array.
Code from this book: Programming Android, 2nd Edition.
At first I touch the screen with two fingers - everything OK.
Then touch the screen with one finger - java.lang.IllegalArgumentException: pointerIndex out of range
public boolean onTouch(View v, MotionEvent event) {
int action = event.getActionMasked();
int idx;
int n;
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
idx = MotionEventCompat.getActionIndex(event);
tracks.add(event.getPointerId(idx));
break;
case MotionEvent.ACTION_POINTER_UP:
idx = MotionEventCompat.getActionIndex(event);
tracks.remove(event.getPointerId(idx));
break;
case MotionEvent.ACTION_MOVE:
n = event.getHistorySize();
for (Integer i : tracks) {
idx = event.findPointerIndex(i.intValue());
for (int j = 0; j < n; j++) {
addDot(
dots,
event.getHistoricalX(idx, j),
event.getHistoricalY(idx, j),
event.getHistoricalPressure(idx, j),
event.getHistoricalSize(idx, j)
);
}
}
break;
default:
return false;
}
for (Integer i: tracks){
idx = event.findPointerIndex(i.intValue());
addDot(
dots,
event.getX(idx),
event.getY(idx),
event.getPressure(idx),
event.getSize(idx)
);
}
return true;
}
The method findPointerIndex() may return -1. From what I have experienced this will happen when you touch with 2 fingers (or more) and let go of the first pressed finger.
I think this is the cause of 'pointerIndex out of range' error - since -1 is not a valid index :-)
For ACTION_MOVE I would recommend using a loop with getPointerCount(), ie looping from 0 to getPointerCount() - 1.
Using this method you can get touch position with getX(finger) and getY(finger).
Where 'finger' is your loop count.
Using this method will not keep track of what ID your finger has, ie if you press with two fingers and let go the first finger (ID=0) then the second finger will continue to count as the first finger (which is now not pressed)
Hope this helps!
/Richard
I am working on a multitouch program that needs to record only the movements made by the second finger or index pointer.
Now the documentation says that we can use MotionEvent.ACTION_POINTER_INDEX_MASK and & it with action and shift by INDEX_SHIFT to get the pointer that made the action like going up or down. But this technique does not work on move.
Is there anyway that we can detect the move action made by a certain pointer alone?
Thx,
yes, you can have something like this in your View class:
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
if(event.getPointerCount()>1){
//where 1 is the index of the second finger
final int Y = event.getY(1);
final int X = event.getX(1);
}
break;
}
}
so depending on what finger you want to get the movent you can set the get to that index. Rember that values may be from 0 (the first pointer that is down) to getPointerCount()-1.
I tested this on 2.2 Gingerbread so I hope it be useful for you :)
You can get the effective pointer index by checking which pointer changed:
private final int MAX_POINTER = 5; // 5 different touch pointers supported on most devices
private float mLastTouchPositionX[];
private float mLastTouchPositionY[];
#Override
public boolean onTouchEvent(MotionEvent aEvent)
int tActionIndex = aEvent.getActionIndex();
int tPointerCount = aEvent.getPointerCount();
/*
* Check which pointer changed on move
*/
if (tMaskedAction == MotionEvent.ACTION_MOVE) {
for (int i = 0; i < tPointerCount && i < MAX_POINTER; i++) {
if (mLastTouchPositionX[i] != aEvent.getX(i) || mLastTouchPositionY[i] != aEvent.getY(i)) {
mLastTouchPositionX[i] = aEvent.getX(i);
mLastTouchPositionY[i] = aEvent.getY(i);
// Found new action index
tActionIndex = i;
break;
}
}
}
...
}
After getting the calculator application to work I decided to try to create pong. There is a box in the center and two paddles on both ends. The phone is horizontal. I have the box bouncing off the walls and the paddle moves with me moving my finger down. My problem is i want to make it two player and i want to have multiple finger input for the game. I want one finger to move paddle 1 and the other to move paddle 2. So far this is my input code
#Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
float p1y = ev.getY();
if(ev.getX()<300)
{
player1y = p1y;
}
if(ev.getX()>300)
{
player2y = p1y;
}
//player1y = p1y;
invalidate();
break;
}
}
return true;
}
it resides in my surfaceview class. How can i modify the input method or completely get rid of it and change it to accomplish my goal? Also sorry about my variables. Eclipse crashes a lot on me and my laptops touch panel tends to move my cursor so shorter variables seemed viable. p1y is the y of the touch. and player1y and player2y is the y positions of the player1 and player2 paddle.
A MotionEvent can hold multiple pointers. Use getPointerCount() to see how many pointers are touching the screen in the current event. There are alternate versions of getX and getY that take an index from 0-getPointerCount() - 1.
In a more complex app you would want to track fingers by pointer ID, but for something this simple where you are using a cutoff point on the screen you could do something like this in your ACTION_MOVE case:
int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
float x = ev.getX(i);
float y = ev.getY(i);
if (x < 300) {
player1y = y;
} else if (x > 300) {
player2y = y;
}
}
This post from the Android Developers Blog might help if you'd like more information: http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html
Can anyone help please.
Im writing a small Android game where the player is able to select a "barrier" and drag it accross the screen with their finger. I have the barriers drawn on the screen and I am able to drag it accross the screen.
My problem however is when I add more than 1 barrier, eg 3 barriers, and drag a barrier accross the screen, they all drag and they all drag to the same position. That is to say they all lie on top of each other, making it look as though there is only 1 barrier.
Here is my code, can anyone please tell me where I am going wrong/explain where I am going wrong.
public class MainGamePanel extends SurfaceView implements SurfaceHolder.Callback, SensorEventListener {
// Initialising the Barrier
private Barrier barrier[] = new Barrier[3];
// The Main Game Panel
public MainGamePanel(Context context) {
super(context);
// Adding a call-back (this) to the surfaceHolder to intercept events
getHolder().addCallback(this);
// Creating the Game items
// The starting coordinates of the Barrier
int x = 30;
int y = 270;
barrier[0] = new Barrier(BitmapFactory.decodeResource(getResources(), R.drawable.blue_barrier), x, y);
barrier[1] = new Barrier(BitmapFactory.decodeResource(getResources(), R.drawable.green_barrier), x + 15, y);
barrier[2] = new Barrier(BitmapFactory.decodeResource(getResources(), R.drawable.pink_barrier), x + 30, y);
// Create the Game Loop Thread
thread = new MainThread(getHolder(), this);
// Make the GamePanel focusable so it can handle events
setFocusable(true);
}
// Handles the touch events
public boolean onTouchEvent(MotionEvent event)
{
int eventAction = event.getAction();
int x = (int)event.getX();
int y = (int)event.getY();
switch (eventAction)
{
// Touch down so check if finger is on Barrier
case MotionEvent.ACTION_DOWN:
if (x > barrier[0].getX() && x < barrier[0].getX() + 8
&& y > barrier[0].getX() && y < barrier[0].getY() + 8)
{
barrier[0].isTouched();
}
else if (x > barrier[1].getX() && x < barrier[1].getX() + 8
&& y > barrier[1].getX() && y < barrier[1].getY() + 8)
{
barrier[1].isTouched();
}
else if (x > barrier[2].getX() && x < barrier[2].getX() + 8
&& y > barrier[2].getX() && y < barrier[2].getY() + 8)
{
barrier[2].isTouched();
}
break;
// Touch-drag with the Barrier
case MotionEvent.ACTION_MOVE:
// Move the Barrier the same as the finger
for (int i = 0; i < barrier.length; i++)
{
if (barrier[i] == barrier[0])
{
barrier[0].setX(x);
barrier[0].setY(y);
} // end if
else if (barrier[i] == barrier[1])
{
barrier[1].setX(x);
barrier[1].setY(y);
}
else if (barrier[i] == barrier[2])
{
barrier[2].setX(x);
barrier[2].setY(y);
} // end else if
} // end for
break;
case MotionEvent.ACTION_UP:
// Finger no longer on Barrier - Do Nothing
break;
}
return true;
}
// Render - Draws the Game Item Bitmaps to the screen
public void render(Canvas canvas)
{
// Set the background to white
canvas.drawColor(Color.WHITE);
barrier[0].draw(canvas);
barrier[1].draw(canvas);
barrier[2].draw(canvas);
}
// Update
// This is the Game's update method
// It iterates through all the Objects and calls their update() methods (if they have one)
public void update()
{
} // end update
In your barrier move code, you aren't checking a particular barrier is touched, so you're moving all of them to the same coordinates. The following loop does exactly the same thing as your current code:
// Move the Barrier the same as the finger
for (int i = 0; i < barrier.length; i++)
{
barrier[i].setX(x);
barrier[i].setY(y);
} //end for
To fix this, you need to check if the current barrier in the loop is the one that was touched, and can change that whole loop to something like:
// Move the Barrier the same as the finger
for (int i = 0; i < barrier.length; i++)
{
if (barrier[i].isTouched())
{
barrier[i].setX(x);
barrier[i].setY(y);
} // end if
} // end for
You would then need to make sure that you un-set the touched property in the ACTION_UP section. If you post your Barrier class definition I can probably help more.
I'm pretty sure your problem could be solved by making the barriers three separate objects instead of in an array.
As already pointed out, your code in handling the move is wrong because it moves all the Barriers to the same X,Y when it should only move the one that is touched.
Also, you are never resetting the isTouched on the objects in the Action up. When the user lifts their finger you should set them all to isTouched == false. If you don't do that then once you touch one it will always move to the X,Y.