Créez un jeu de type snooker avec une animation basée sur la physique Android
Wtout en jouant avec API d’animation basée sur la physique Android, Je viens de créer une application simple (une seule classe d’activité – pas une bonne pratique de programmation, mais pour dire à quel point c’est simple) avec environ 200 lignes de codes, et j’ai le jeu de snooker.
Animation basée sur la physique Android
L’animation basée sur la physique Android se compose de ces deux animations, à savoir FlingAnimation
et SpringAnimation
, qui est une sous-classe de DynamicAnimation
.
Il n’est pas fourni par défaut sur Android. Vous devrez passer les dépendances build.gradle
en ajoutant
implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"
Contrairement à l’animation normale, les animations Fling et Spring se comportent en fonction de la vitesse qui leur est fournie. Le comportement est donc dynamique en fonction de sa vitesse de départ.
Je les utilise tous les deux et je les ai attachés ensemble pour créer ce jeu de type snooker simple. Consultez ci-dessous pour un peu plus de détails sur la façon dont il est mis en œuvre.
Le seul contrôle tactile que j’ajoute est le gestureDetector
img_ball.setOnTouchListener { view, motionEvent ->
gestureDetector.onTouchEvent(motionEvent)
true
}
En détectant le geste Fling, nous pourrions alors obtenir à la fois la vitesse X et Y. En utilisant ces vitesses, nous allons commencer la 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
}
}
C’est le comportement le plus important du jeu. On pourrait simplement cliquer et faire glisser la balle vers la direction dans laquelle elle devrait se diriger et la vitesse du lancer en fonction du geste de l’utilisateur.
Contrairement à toute autre animation, le développeur ne définit pas la destination finale cible, mais à la place, elle est déterminée par la vitesse fournie avec le paramètre de friction.
Remarquez le GIF ci-dessous montrant comment la balle s’écoule le long de la force de lancer de l’utilisateur.
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()
}
}
}
L’animation Fling ci-dessus est appliquée à la fois aux directions X et Y individuellement.
- Les deux sont définis
DEFAULT_FRICTION
pour qu’il soit tout aussi lisse - Réglez Max et Min pour ne pas dépasser la taille de l’écran
- Quand l’aventure se termine s’il y a
velocity
(ne se produit que lorsqu’il atteint la limite de l’écran), il continuera avec l’animation de printemps - Pendant le déplacement, vérifiez constamment s’il pénètre déjà dans le trou lorsque la vitesse est inférieure à un certain seuil. La balle qui roule vite n’entre pas dans le trou. (remarquez que l’un des plans ci-dessus roulera sur le trou)
Dans le vrai snooker, il n’y aura pas de printemps, mais la balle rebondira. Mais juste pour pratiquer le comportement de Spring dans l’animation basée sur la physique Android, je fais rebondir le ressort à billes quand il frappe le mur.
Découvrez le GIF ci-dessous. Il montre la balle qui rebondit au printemps après avoir heurté le mur.
Le Spring comme indiqué dans le code Fling ci-dessus est déclenché à la fin de 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()
}
}
Il devrait également démarrer l’animation du printemps quand il reste encore de la vitesse du Fling à passer.
Autre que la vitesse, il y a le SpringAnimation qui prend le SpringForce à condition de. Ces deux éléments seront développés ci-dessous. le SpringForce est également utilisé pour définir la position finale de la balle pour qu’elle se stabilise après l’effet Spring.
Force du printemps
Au printemps, il a une rigidité (à quel point il est flexible) et un rapport d’amortissement (à quel point il est rebondissant). Il s’agit d’un nombre flottant avec quelques constantes prédéfinies que nous pouvons utiliser. Ils sont définis dans le SpringForce.
private val springForce: SpringForce
get() = SpringForce(0f).apply {
stiffness = SpringForce.STIFFNESS_LOW
dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY
}
Autre que raideur et rapport d’amortissement, on peut également définir la destination finale, que nous avons vu définie lorsque le SpringAnimation a commencé.
Animation de printemps
Le SpringAnimation définit l’attribut du Spring et recevra le SpringForce. Cependant, le SpringForce n’est pas configuré lors de l’initialisation, car nous n’en connaissons pas encore la destination finale (c’est-à-dire quelle frontière il atteindra).
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()
}
}
}
L’animation de ressort ci-dessus est appliquée aux directions X et Y individuellement. (au cas où il heurte un mur d’angle, il peut rebondir dans les deux sens). Tout comme Fling Animation, il vérifie également s’il a touché le trou et a une vitesse suffisamment lente pour entrer dans le trou.
Il y a diverses considérations avant de pouvoir déterminer s’il est entré dans le trou.
Est-ce à une vitesse suffisamment lente?
Ceci est important car une balle qui se déplace rapidement ne s’arrêtera pas pour entrer dans un trou car la gravité ne parvient pas à la tirer vers le bas.
private fun isSlowEnoughToEnterHole(): Boolean { return
abs(velocityFlingY) < VELOCITY_THRESHOLD &&
abs(velocityFlingX) < VELOCITY_THRESHOLD &&
abs(velocitySpringY) < VELOCITY_THRESHOLD &&
abs(velocitySpringX) < VELOCITY_THRESHOLD
}
Pour vérifier la vitesse, nous devons vérifier les vitesses de toutes les animations pour nous assurer que toutes les vitesses sont sous le seuil. Cela permet d’éviter que l’on pénètre dans un trou simplement parce qu’il ne se déplace pas (vitesse 0) dans une certaine direction uniquement mais se déplace rapidement dans une autre direction.
Entre-t-il dans le trou?
Pour vérifier s’il pénètre dans le trou, nous devons obtenir le centre de l’appel comme mesure. Et aussi le isEnding
le contrôle est là au cas où s’il y a plusieurs directions, il est assez lent pour entrer dans le trou, nous avons juste besoin d’avoir un contrôle.
private fun endCheck() {
if (isEnding) returnval 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()
}
}
Après avoir obtenu le centre des coordonnées de la balle, nous devons voir si les coordonnées se trouvent dans le trou. J’utilise une simple correspondance rectangulaire. Idéalement, on devrait utiliser un rayon pour calculer pour plus de précision. Néanmoins, l’erreur est assez petite.
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)
Après être entré dans le trou, nous devons mettre fin à toutes les animations mais effectuer une nouvelle animation qui montre la balle entrant dans le trou.
private fun isEnteringHole(hole: ImageView, ballCenterX: Float, ballCenterY: Float): Boolean {
if (isHittingTarget(ballCenterX, hole, ballCenterY)) {
isEnding = true
endAllPhysicAnimation()
animateBallIntoHole(hold)
return true
}
return false
}
Animez la balle entrant dans le trou.
Enfin, pour avoir une belle façon de montrer la balle entrant dans le trou, nous effectuons plusieurs animations.
- Animation de traduction, qui déplace les positions X et Y de la balle vers le centre du trou.
- Effectuer un rétrécissement (mise à l’échelle inverse) de la balle avec la décoloration (réduire la transparence de la balle), ce qui donne l’impression de tomber dans le trou
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()
}
L’animation est terminée, puis montre la balle réapparaître au milieu.
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()
}
Pour en savoir plus sur les animations d’animation de base, consultez les deux blogs ci-dessous.
Lorsque nous les combinons tous ensemble, avec l’animation Fling, Spring et Entering the hole ensemble, les éléments ci-dessous résument bien tout cela.
- La balle jette directement dans le trou du coin
- Frapper le trou d’angle, l’effet Spring a lieu à la fois dans les axes X et Y
- Et puis encerclez lentement contre le trou et entrez dans le trou avec translation.
Un petit détail non mentionné ci-dessus est, à la fin, nous arrêtons l’animation de printemps et ralentissons l’animation Fling en augmentant le frottement à un nombre plus élevé.
private fun endAllPhysicAnimation() {
springAnimationX.cancel()
springAnimationY.cancel()
flingAnimationY.friction = BREAK_FRICTION
flingAnimationX.friction = BREAK_FRICTION
stopAnimation = true
}
Par conséquent, cela produit un joli ralentissement du mouvement autour du trou et avec un mélange de Fling et Spring et Translation, montrant l’effet agréable.