ARCore Sceneform SDK: Getting Started
In this tutorial, you’ll learn how to make augmented reality Android apps with ARCore using Sceneform. By Dario Coletto.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
ARCore Sceneform SDK: Getting Started
25 mins
- Setting Up the Environment
- Using a Real Device
- Using an Emulator
- Getting the Sceneform Plugin
- Getting Started
- Creating 3D Objects
- Making a 3D Object From Scratch
- Instantiating an Existing 3D Model
- Displaying XML in Real Life
- Adding Another Light
- Interacting With the AR World
- Touch Listener on a 3D Object
- Animating
- Animating a Node
- Creating an Animated Node
- Completing the Project
- Where to Go From Here?
Animating
There are two ways to create an animation: animate an existing node or create a custom node capable of complex animations.
Animating a Node
Animating an existing node can be achieved using an ObjectAnimator
and is as easy as setting a start and an end value for a property.
ObjectAnimator
is an Android class, and it’s not specific to ARCore or Sceneform. If you don’t know how to use it, we have you covered! You can read more about it in the Android Animation Tutorial with Kotlin.
For your game, you’re going to use the animator to blink a light, so you’ll have to pass four parameters:
- A target (the light object).
- A property (the intensity of the light).
- The start value of the property.
- The end value of the same property.
There is an extension method in Extensions.kt that uses an ObjectAnimator
to blink a light:
private fun failHit() {
scoreboard.score -= 50
scoreboard.life -= 1
failLight?.blink()
...
}
Next, you need to animate the droid. Add the following code to the TranslatableNode
class to create the up animation for the droid renderable:
class TranslatableNode : Node() {
...
// 1
var position: DroidPosition = DroidPosition.DOWN
// 2
fun pullUp() {
// If not moving up or already moved up, start animation
if (position != DroidPosition.MOVING_UP && position != DroidPosition.UP) {
animatePullUp()
}
}
// 3
private fun localPositionAnimator(vararg values: Any?): ObjectAnimator {
return ObjectAnimator().apply {
target = this@TranslatableNode
propertyName = "localPosition"
duration = 250
interpolator = LinearInterpolator()
setAutoCancel(true)
// * = Spread operator, this will pass N `Any?` values instead of a single list `List<Any?>`
setObjectValues(*values)
// Always apply evaluator AFTER object values or it will be overwritten by a default one
setEvaluator(VectorEvaluator())
}
}
// 4
private fun animatePullUp() {
// No matter where you start (i.e. start from .3 instead of 0F),
// you will always arrive at .4F
val low = Vector3(localPosition)
val high = Vector3(localPosition).apply { y = +.4F }
val animation = localPositionAnimator(low, high)
animation.addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
position = DroidPosition.UP
}
override fun onAnimationStart(animation: Animator?) {
position = DroidPosition.MOVING_UP
}
})
animation.start()
}
}
Here, you have added:
- A property to track the position of the droid.
- A method to pull the droid up.
- An
ObjectAnimator
to animate the droid. - A private method to perform the up animation.
Calling the start()
method on the animator is enough to fire it, and the cancel()
method will stop the animation. Setting auto cancel to true will stop an ongoing animation when a new one — with the same target and property — is started.
Try to write the pullDown
method and associated code yourself. If you’re having trouble, open the spoiler below for the complete code.
[spoiler title=”Pull down animation code”]
fun pullDown() {
// If not moving down or already moved down, start animation
if (position != DroidPosition.MOVING_DOWN && position != DroidPosition.DOWN) {
animatePullDown()
}
}
private fun animatePullDown() {
// No matter where you start,
// you will always arrive at 0F
val high = Vector3(localPosition)
val low = Vector3(localPosition).apply { y = 0F }
val animation = localPositionAnimator(high, low)
animation.addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
position = DroidPosition.DOWN
}
override fun onAnimationStart(animation: Animator?) {
position = DroidPosition.MOVING_DOWN
}
})
animation.start()
}
[/spoiler]
Creating an Animated Node
Custom animated nodes are a little bit harder to use than using ObjectAnimator
as you have done above, but they are more powerful.
To use custom animation with a node, you need to extend the Node
class and then override the onUpdate
method.
WhacARDroid doesn’t need anything that customizable, so if you want to check out an example you can see Google’s Sceneform sample.
Completing the Project
You’re nearly done! The last part covers starting the real game.
Inside scoreboard.onStartTapped
in the MainActivity initResources()
method, add this code to start a Runnable
:
scoreboard.onStartTapped = {
...
// Start the game!
gameHandler.post {
repeat(MOVES_PER_TIME) {
gameHandler.post(pullUpRunnable)
}
}
}
Next, create the pullUpRunnable
Runnable property for MainActivity.
private val pullUpRunnable: Runnable by lazy {
Runnable {
// 1
if (scoreboard.life > 0) {
grid.flatMap { it.toList() }
.filter { it?.position == DOWN }
.run { takeIf { size > 0 }?.getOrNull((0..size).random()) }
?.apply {
// 2
pullUp()
// 3
val pullDownDelay = (MIN_PULL_DOWN_DELAY_MS..MAX_PULL_DOWN_DELAY_MS).random()
gameHandler.postDelayed({ pullDown() }, pullDownDelay)
}
// 4
// Delay between this move and the next one
val nextMoveDelay = (MIN_MOVE_DELAY_MS..MAX_MOVE_DELAY_MS).random()
gameHandler.postDelayed(pullUpRunnable, nextMoveDelay)
}
}
}
Its purposes are to:
- Check if the game is completed.
- Pull up a random droid renderable from the grid.
- Pull the same droid down after a random delay.
- If player has at least one life, start itself over again after a random delay.
Next, you need to handle what happens if the player hits the droid.
If the droid is down, it counts as a missed hit, so you remove 50 points and a life from the player. If it’s up, add 100 points to the player.
In MainActivity, in the code within arFragment.setOnTapArPlaneListener
, there is a setOnTapListener
that is currently just passed a TODO comment in it’s lambda. Replace the comment with this logic:
this.setOnTapListener { _, _ ->
if (this.position != DOWN) {
// Droid hit! assign 100 points
scoreboard.score += 100
this.pullDown()
} else {
// When player hits a droid that is not up
// it's like a "miss", so remove 50 points
failHit()
}
}
As a final step, whenever the player fails a hit, if its life counter is equal to zero or less, reset every droid on the grid by calling the pullDown
method. Update the failHit()
method to be as follows:
private fun failHit() {
scoreboard.score -= 50
scoreboard.life -= 1
failLight?.blink()
if (scoreboard.life <= 0) {
// Game over
gameHandler.removeCallbacksAndMessages(null)
grid.flatMap { it.toList() }
.filterNotNull()
.filter { it.position != DOWN && it.position != MOVING_DOWN }
.forEach { it.pullDown() }
}
}
Everything is ready! Run your brand new WhacARDroid game and challenge your friends to beat your score. :]