Make Snooker-like Game with Android Physics-based Animation
Tags: Software Development, Mobile App Development, Game Development, Android App Development, Programming

While playing around with Android Physics-based Animation API, I just created a simple App (just one Activity class— not good programming practice, but to state how simple it is) with about 200 lines of codes, and have the snooker like game.

Android Physics-Based Animation

Android Physics-Based Animation consists of these two animations, i.e. FlingAnimation and SpringAnimation , which is a subclass of DynamicAnimation .

It is not provided by default on Android. You’ll need to get the dependencies through build.gradle by adding

implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"

Unlike normal animation, Fling and Spring animation behave according to the velocity provided to them. Hence the behavior is dynamic in correspond to its start velocity.

I use both of them and tied them together to make this simple snooker-like game. Check out below for a little more details of how it is implemented.

Gesture Detection

The only touch control I add on is the gestureDetector

img_ball.setOnTouchListener { view, motionEvent ->

gestureDetector.onTouchEvent(motionEvent)

true

}

Upon detecting the Fling gesture, we could then get both the X and Y velocity. Using those velocities, we’ll start the FlingAnimations

private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {

override fun onDown(e: MotionEvent?): Boolean {

stopAnimation = false

return true

}



override fun onFling(

e1: MotionEvent?,

e2: MotionEvent?,

velocityX: Float,

velocityY: Float

): Boolean {

if (isAnimationRunning()) return false

flingAnimationX.setStartVelocity(velocityX).start()

flingAnimationY.setStartVelocity(velocityY).start()

return true

}

}

Fling Dynamic Animation

This is the most important behavior of the game. One could just click and swipe the ball towards the direction it should head and the velocity of the fling based on the gesture of the user.

Unlike any other animation, the developer doesn’t define the target final destination, but instead, it is determined by the velocity provided with the frictions setting.

Notice the below GIF showing how the ball flow along the per the fling strength of the user.

private fun instantiateFlingAnimation(

max: Float, animationType: DynamicAnimation.ViewProperty,

springAnimation: SpringAnimation, resetVelocity: (Float) -> Unit

): FlingAnimation {

return FlingAnimation(img_ball, animationType)

.setFriction(DEFAULT_FRICTION).apply {

setMinValue(0f)

setMaxValue(max)

addEndListener { _, _, _, velocity ->

resetVelocity(0f)

startStringAnimation(

velocity, springAnimation, springForce, max)

}

addUpdateListener { _, _, velocity ->

resetVelocity(velocity)

if (isSlowEnoughToEnterHole())

endCheck()

}

}

}

The Fling Animation above is applied to both X and Y direction individually.

Both of them are set the DEFAULT_FRICTION so that it is equally smooth

so that it is equally smooth Set Max and Min so it doesn’t go beyond the screen size

When the fling ends if there’s velocity (only happens when it hits the boundary of the screen), it will continue on with the Spring Animation

(only happens when it hits the boundary of the screen), it will continue on with the Spring Animation While moving, constantly check if it already enters the hole when the velocity is slower than a certain threshold. Fast-rolling ball won’t enter the hole. (notice one of the shots above will roll over the hole)

Spring Dynamic Animation

In actual snooker, there won’t be any Spring, instead, the ball will bounce back. But just to practice the behavior of Spring in Android Physics-Based Animation, I make the ball spring bouncing when it hits the wall.

Check out the GIF below. It shows the ball bouncing in spring-like after hitting the wall.

The Spring as shown in the Fling code above is triggered at the end of Fling.

private fun startStringAnimation(

velocity: Float, springAnimation: SpringAnimation,

springForce: SpringForce, max: Float

) {

if (abs(velocity) > 0 && !stopAnimation) {

springAnimation

.setSpring(springForce.setFinalPosition(

if (velocity > 0) max else 0f))

.setStartVelocity(velocity)

.start()

}

}

It should also start the Spring Animation when there’s still remaining velocity from the Fling to pass over.

Other than the velocity, there’s the SpringAnimation which takes the SpringForce provided. These two will be further elaborated below. The SpringForce is also used to set the final position of the ball to settle down after the Spring effect.

Spring Force

In Spring, it has a Stiffness (how flexible it is) the and Damping Ratio (how bouncy it is). It is a float number settings with some constants predefined we can use. They are defined in the SpringForce.

private val springForce: SpringForce

get() = SpringForce(0f).apply {

stiffness = SpringForce.STIFFNESS_LOW

dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY

}

Other than stiffness and dampingRatio, one can set the final destination on it as well, which we have seen set when the SpringAnimation began.

Spring Animation

The SpringAnimation define the attribute of the Spring, and will be given the SpringForce. However, the SpringForce is not setup during initialization because we haven’t know the final destination of it yet (i.e. which border it will hit).

private fun instantiateSpringAnimation(

animationType: DynamicAnimation.ViewProperty?,

resetVelocity: (Float) -> Unit

): SpringAnimation {

return SpringAnimation(img_ball, animationType).apply {

addEndListener { _, _, _, _ -> resetVelocity(0f) }

addUpdateListener { _, _, velocity ->

resetVelocity(velocity)

if (isSlowEnoughToEnterHole())

endCheck()

}

}

}

The Spring Animation above is applied to both X and Y direction individually. (in case it hits a corner wall, it can bounce both ways). Just like Fling Animation, it also checks if it has hit the hole, and has velocity slow enough to enter the hole.

Entering Hole

There are various considerations before one could determine if it has entered the hole.

Is it at a slow enough velocity?

This is important as a fast-moving ball won’t stop to enter a hole as the gravity fails to pull it down.

private fun isSlowEnoughToEnterHole(): Boolean { return

abs(velocityFlingY) < VELOCITY_THRESHOLD &&

abs(velocityFlingX) < VELOCITY_THRESHOLD &&

abs(velocitySpringY) < VELOCITY_THRESHOLD &&

abs(velocitySpringX) < VELOCITY_THRESHOLD

}

To verify the speed, we need to verify the velocities from all the animations to ensure the velocities of them all are under the threshold. This is to avoid one entering a hole just because it is not moving (0 velocity) in a certain direction only but moving fast in another direction.

Is it entering the hole?

To check if it is entering the hole, we need to get the center of the call as the measurement. And also the isEnding check is there in case if there are multiple directions it is slow enough to enter the hole, we just need to have one check.

private fun endCheck() {

if (isEnding) return



val ballCenterX = img_ball.x + img_ball.width / 2

val ballCenterY = img_ball.y + img_ball.height / 2



if (holes.any { isEnteringHole(it, ballCenterX, ballCenterY) }){

Toast.makeText(this, "Congratulation!",

Toast.LENGTH_SHORT).show()

}

}

After getting the center of the ball coordinate, we need to see if the coordinate is within the hole. I use simple rectangular matching. Ideally one should use a radius to calculate for more accuracy. Nonetheless, the error is pretty small.

private fun isHittingTarget(ballCenterX: Float, hole: ImageView, ballCenterY: Float) =

(ballCenterX >= hole.x && ballCenterX <= hole.x + hold.width) &&

(ballCenterY >= hole.y && ballCenterY <= hole.y + hold.height)

After entering the hole, we need to end all animation but perform a new animation that shows the ball entering the hole.

private fun isEnteringHole(hole: ImageView, ballCenterX: Float, ballCenterY: Float): Boolean {

if (isHittingTarget(ballCenterX, hole, ballCenterY)) {

isEnding = true

endAllPhysicAnimation()

animateBallIntoHole(hold)

return true

}

return false

}

Animate the ball entering the hole.

Lastly, to have a nice way of showing the ball entering the hole, we perform several animations.

Translation animation, that moves both X and Y position of the ball towards the center of the hole. Perform a shrinking (inverse scaling) of the ball together with the fading away (reduce the transparency of the ball), which makes it like dropping into the hole

private fun animateBallIntoHole(hole: ImageView) {

AnimatorSet().apply {

play(

ObjectAnimator.ofPropertyValuesHolder(

img_ball,

PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f),

PropertyValuesHolder.ofFloat(View.SCALE_X, 1f, 0.5f),

PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f, 0.5f)

)

).after(

ObjectAnimator.ofPropertyValuesHolder(

img_ball,

PropertyValuesHolder.ofFloat(View.X, hole.x),

PropertyValuesHolder.ofFloat(View.Y, hole.y)

)

)



addListener(object : AnimatorListenerAdapter() {

override fun onAnimationEnd(animation: Animator?) {

reappearBall()

}

})



}.start()

}

The animation completed, then show the ball re-appear in the middle.

private fun reappearBall() {

img_ball.translationX = 0f

img_ball.translationY = 0f

img_ball.scaleX = 1f

img_ball.scaleY = 1f

flingAnimationX.friction = DEFAULT_FRICTION

flingAnimationY.friction = DEFAULT_FRICTION

ObjectAnimator.ofFloat(img_ball, View.ALPHA, 0f, 1f).apply {

addListener(object : AnimatorListenerAdapter() {

override fun onAnimationEnd(animation: Animator?) {

isEnding = false

}

})

}.start()

}

To get some read about basic animator animations, check out the below two blogs.

Putting all together

When we combine them all together, with the Fling, Spring, and the Entering the hole animation together, the below sum it all nicely.

The ball flings straight into the corner hole Hitting the corner hole, the Spring effect take place both in X and Y axis And then slowly circle against the hole and entering the hole with translation.

One little detail didn’t mention above is, upon ending, we stop the Spring Animation, and slow down the Fling Animation by increasing the friction to a higher number.

private fun endAllPhysicAnimation() {

springAnimationX.cancel()

springAnimationY.cancel()

flingAnimationY.friction = BREAK_FRICTION

flingAnimationX.friction = BREAK_FRICTION

stopAnimation = true

}

Hence this produces the nice slow down of movement around the hole and with a mixture of Fling and Spring and Translation, showing the nice effect.