Thursday, March 14, 2013

Basic momentum scrolling implementation with AndEngine

This is the simple implementation of momentum scrolling with AndEngine I did for Word Drift, a word puzzle game I made a few months ago.

It is working OK, so if you want to use it as-is, feel free to do so.  If you want to modify it, please do, but if possible, please post the modify code in the comment so everybody can benefit.

If you are actually thinking about modifying it, please do low/high pass filter to smooth out the accel value.  Currently I just average out the accel value with its previous two values.

Once that's done, maybe a rubber-band effect or something similar to it.

Here's how it looks.




Here's the code.


public class MainActivity extends SimpleBaseGameActivity implements IScrollDetectorListener, IOnSceneTouchListener {
 
 private static final float FREQ_D = 120.0f;
 
 private static final int STATE_WAIT = 0;
 private static final int STATE_SCROLLING = 1;
 private static final int STATE_MOMENTUM = 2;
 private static final int STATE_DISABLE = 3;
 
 private static final int WRAPPER_HEIGHT = 1260;
 private static final float MAX_ACCEL = 5000;

 private static final double FRICTION = 0.96f;
 
 private TimerHandler thandle;
 private int mState = STATE_DISABLE;
 private double accel, accel1, accel0;
 private float mCurrentY;
 private IEntity mWrapper;
 private SurfaceScrollDetector mScrollDetector;
 private long t0;
 
 @Override
 public EngineOptions onCreateEngineOptions() {
  final Camera camera = new Camera(0, 0, 480, 800);
  final EngineOptions eo = new EngineOptions(true, ScreenOrientation.PORTRAIT_SENSOR, 
       new RatioResolutionPolicy(480, 800), camera);
  return eo;
 }

 @Override
 protected void onCreateResources() {
  thandle = new TimerHandler(1.0f / FREQ_D, true, new ITimerCallback() {
   @Override
   public void onTimePassed(final TimerHandler pTimerHandler) {
    doSetPos();
   }
  });
 }

 @Override
 protected Scene onCreateScene() {
  Scene scene = new Scene();
  scene.setBackground(new Background(0.1f, 0.1f, 0.1f));
  
  mScrollDetector = new SurfaceScrollDetector(this);
  scene.setOnSceneTouchListener(this);
  
  mWrapper = new Entity(0, 0);
  for (int i = 0; i < 20; i++) {
   Rectangle r = new Rectangle(17.5f, i * 100 + 20, 445, 80, getVertexBufferObjectManager());
   mWrapper.attachChild(r);
  }
  scene.attachChild(mWrapper);
  scene.registerUpdateHandler(thandle);
  mState = STATE_WAIT;
  return scene;
 }

 @Override
 public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) {
  if (mState == STATE_DISABLE)
   return true;
  
  if (mState == STATE_MOMENTUM) {
   accel0 = accel1 = accel = 0;
   mState = STATE_WAIT;
  }

  mScrollDetector.onTouchEvent(pSceneTouchEvent);
  return true;
 }

 @Override
 public void onScrollStarted(ScrollDetector pScollDetector, int pPointerID, float pDistanceX, float pDistanceY) {
  t0 = System.currentTimeMillis();
  mState = STATE_SCROLLING;
 }

 @Override
 public void onScroll(ScrollDetector pScollDetector, int pPointerID, float pDistanceX, float pDistanceY) {
  long dt = System.currentTimeMillis() - t0;
  if (dt == 0)
   return;
  double s =  pDistanceY / (double)dt * 1000.0;  // pixel/second
  accel = (accel0 + accel1 + s) / 3;
  accel0 = accel1;
  accel1 = accel;
  
  t0 = System.currentTimeMillis();
  mState = STATE_SCROLLING;
  
 }

 @Override
 public void onScrollFinished(ScrollDetector pScollDetector, int pPointerID, float pDistanceX, float pDistanceY) {
  mState = STATE_MOMENTUM;
 }

 protected synchronized void doSetPos() {
  
  if (accel == 0) {
   return;
  }
  
  if (mCurrentY > 0) {
   mCurrentY = 0;
   mState = STATE_WAIT;
   accel0 = accel1 = accel = 0;
  }
  if (mCurrentY < -WRAPPER_HEIGHT) {
   mCurrentY = -WRAPPER_HEIGHT;
   mState = STATE_WAIT;
   accel0 = accel1 = accel = 0;
  }
  mWrapper.setPosition(0, mCurrentY);
  

  
  if (accel < 0 && accel < -MAX_ACCEL)
   accel0 = accel1 = accel = - MAX_ACCEL;
  if (accel > 0 && accel > MAX_ACCEL)
   accel0 = accel1 = accel = MAX_ACCEL;
  
  double ny = accel / FREQ_D;
  if (ny >= -1 && ny <= 1) {
   mState = STATE_WAIT;
   accel0 = accel1 = accel = 0;
   return;
  }
  if (! (Double.isNaN(ny) || Double.isInfinite(ny)))
   mCurrentY += ny;
  accel = (accel * FRICTION);
 }


}

3 comments:

  1. Hey man, great stuff, congratulations! How would that work in GLES2-AnchorCenter?
    All the best,
    Fabiano

    ReplyDelete
    Replies
    1. Just change mCurrentY += ny; to mCurrentY -= ny; and it will work perfectly!

      Delete
  2. Or you cen change mwrapper.setposition(0, mCurrentY) to mwrapper.setY(-mCurrentY) - its better this way if you use it for landscape mode and you want to set x for wrapper.

    ReplyDelete