In this tutorial, we’ll add battle animations and sounds to make the game more fun to play with.
This is the final tutorial of a three-part series on creating a Pokemon battle game with React Native. These are the topics covered in this series:
This tutorial has the same prerequisites as part two of the series.
Since we’ve already implemented most of the features of the app, we can now focus on aesthetics. In this part, we’ll add animations and sounds to make it more interesting and pleasing to play with.
Here are the sounds that we’re going to add:
Here are the animations that we’re going to implement:
We’re going to add the sounds first before the animations, as they’re easier to implement.
We’ll be using the Audio API provided by Expo to play sounds, and React Native’s animation library to implement the animations.
We’ll add background music to each of the screens. We’ll be using the sounds from khinsider.com. Specifically, we’ll use the following soundtracks:
opening.mp3
.final-road.mp3
.rival.mp3
.Open the links above and download the .mp3
file. Create a sounds/background
folder inside src/assets
and copy the files you downloaded in there.
You can also copy the files from the repo.
Open the login screen file, and import the Audio
package from Expo:
1// src/screens/LoginScreen.js 2 import CustomText from "../components/CustomText"; 3 4 import { Audio } from "expo"; // add this
Next, add an initial value for the reference to the background sound. We need it as a class variable so we could stop the sound later once the user logs in:
1constructor(props) { 2 super(props); 3 this.backgroundSound = null; // add this 4 }
Next, add a componentDidMount
method with the following code:
1async componentDidMount() { 2 try { 3 this.backgroundSound = new Audio.Sound(); 4 await this.backgroundSound.loadAsync( 5 require("../assets/sounds/background/opening.mp3") 6 ); // load the mp3 file 7 await this.backgroundSound.setIsLoopingAsync(true); // make the sound loop after it's done playing 8 await this.backgroundSound.playAsync(); // start playing the sound 9 } catch (error) { 10 console.log("error loading background sound: ", error); 11 } 12 } 13 14 render () { 15 // existing code here... 16 }
In the code above, we’re using the async/await pattern to load and play the background sound. To use the async/await pattern, we add the async
keyword before the parent function name or the before the function’s open and close parenthesis, if it’s an anonymous function. Inside the function, we can use the await
keyword to wait for the promise to resolve before executing the next line of code. This essentially makes the asynchronous function behave as if it were a synchronous one.
The loadAsync
method accepts a reference to a file through the require
method. This is the same method we’re using to load images in React Native. Most methods in the Expo Audio API are async. This means you either have to use a promise or callback function to get its response. That’s the reason why we need to put the await
keyword at the beginning of each method call, so each method will wait for the results of the previous method call before proceeding.
Next, update the login
method so it stops the sound once the user logs in. We need to stop the sound because it doesn’t automatically stop once a new sound starts playing. As mentioned earlier, each screen will have its own background sound. That’s why we need to stop it before the sound in the next screen starts playing:
1login = () => { 2 let username = this.state.username; 3 4 if (username) { 5 this.props.navigation.navigate("TeamSelect", { 6 username 7 }); 8 9 this.backgroundSound.stopAsync(); // add this 10 } 11 };
Do the same for the team selection screen. Be sure to load the correct .mp3
file:
1// src/screens/TeamSelectionScreen.js 2 import Pusher from "pusher-js/react-native"; 3 4 import { Audio } from "expo"; // add this 5 6 7 constructor(props) { 8 // previous code here.. 9 10 this.backgroundSound = null; // add this 11 } 12 13 14 async componentDidMount() { 15 try { 16 this.backgroundSound = new Audio.Sound(); 17 await this.backgroundSound.loadAsync( 18 require("../assets/sounds/background/final-road.mp3") 19 ); 20 await this.backgroundSound.setIsLoopingAsync(true); 21 await this.backgroundSound.playAsync(); 22 } catch (error) { 23 console.log("error loading background sound: ", error); 24 } 25 }
Lastly, do the same for the battle screen:
1// src/screens/BattleScreen.js 2 import { Ionicons } from "@expo/vector-icons"; 3 4 import { Audio } from "expo"; // add this 5 6 7 constructor(props) { 8 // previous code here.. 9 10 this.backgroundSound = null; // add this 11 } 12 13 14 async componentDidMount() { 15 // previous code here.. 16 17 // add this 18 try { 19 this.backgroundSound = new Audio.Sound(); 20 await this.backgroundSound.loadAsync( 21 require("../assets/sounds/background/rival.mp3") 22 ); 23 await this.backgroundSound.setIsLoopingAsync(true); 24 await this.backgroundSound.playAsync(); 25 } catch (error) { 26 console.log("error loading background sound: ", error); 27 } 28 }
When a user switches to a specific Pokemon on their team or their Pokemon faints, we want to play their cry.
Download their cry (.mp3
file) from the asset folder of the Pokemon Showdown website. Once downloaded, create a cries
folder inside src/assets/sounds
and copy the files you downloaded over to that folder.
Next, update the src/data/pokemon_data.js
file so it includes a cry
property for each Pokemon. We need to do this because we can’t really pass a variable to the require
function. You can simply copy the contents of the file in the repo if you want. Just be sure the filenames are the same.
At this point, we’re now ready to add the cry sounds. Let’s first add the code for playing the cry when the user switches to another Pokemon. Start by importing the Audio
package:
1// src/components/PokemonOption/PokemonOption.js 2 import { connect } from "react-redux"; 3 4 import { Audio } from "expo"; // add this
Next, we need to convert the component into a class-based one:
1class PokemonOption extends Component { 2 render() { 3 const { pokemon_data, is_selected, action_type } = this.props; // add this 4 5 // add the same return code here.. 6 } 7 }
Note that we’re extracting fewer props in the code above. This is because we’ll be separating the event handler for the onPress
event of the TouchableOpacity
component.
As mentioned earlier, playing Audio requires the direct parent function to have the async
keyword. While you can actually do it like the one below, it’s better if we just refactor the code to declare the function for handling the onPress
event separately:
1<TouchableOpacity onPress={async () => { 2 // same code here.. 3 }}>
To refactor the code, copy the existing code inside onPress
.
Next, create a selectPokemon
function and paste the existing code inside it. Above the existing code, add the props that were previously being extracted:
1render() { 2 // same code here.. 3 } 4 5 // add this 6 selectPokemon = async () => { 7 // add these: 8 const { 9 pokemon_data, 10 is_selected, 11 action_type, 12 togglePokemon, 13 setPokemon, 14 setMessage, 15 setMove, 16 backToMove, 17 opponents_channel 18 } = this.props; 19 20 const { id, cry } = pokemon_data; // add this 21 22 // paste existing code here... 23 24 };
Next, update the code you just pasted to play the cry sound when the action_type
is switch-pokemon
:
1if (action_type == "select-pokemon") { 2 // previous code here.. 3 } else if (action_type == "switch-pokemon") { 4 // previous code here.. 5 6 // add these: 7 try { 8 let crySound = new Audio.Sound(); 9 await crySound.loadAsync(cry); 10 await crySound.playAsync(); 11 } catch (error) { 12 console.log("error loading cry: ", error); 13 } 14 15 // same code: 16 setTimeout(() => { 17 setMessage("Please wait for your turn..."); 18 setMove("wait-for-turn"); 19 }, 2000); 20 }
Next, we need to update the MovesList component so it plays the cry sound when the opponent Pokemon faints:
1// src/components/MovesList/MovesList.js 2 3 import { connect } from "react-redux"; 4 5 import { Audio } from "expo"; // add this
Just like what we did with the PokemonOption component earlier, we also need to refactor this component into a class-based one:
1class MovesList extends Component { 2 render() { 3 const { moves } = this.props; 4 5 // add existing return code here.. 6 } 7 }
Next, copy the code inside the onPress
handler, then update it to use a named function. Pass in the item
from the FlatLists’ renderItem
method as an argument so we could make use of it inside the selectMove
function:
1<TouchableOpacity 2 style={styles.container} 3 onPress={this.selectMove.bind(this, item)} 4 > 5 <CustomText styles={styles.label}>{item.title}</CustomText> 6 </TouchableOpacity>
Add the selectMove
function and paste the code from onPress
:
1selectMove = async item => { 2 // add these: 3 const { 4 moves, 5 opponent_pokemon, 6 setOpponentPokemonHealth, 7 8 backToMove, 9 pokemon, 10 setMessage, 11 setMove, 12 removePokemonFromOpponentTeam, 13 setOpponentPokemon, 14 opponents_channel 15 } = this.props; 16 17 // paste existing onPress code here.. 18 19 }
Lastly, when the opponent’s Pokemon faints, play the cry sound:
1if (health < 1) { 2 // existing code here.. 3 4 // add these: 5 try { 6 let crySound = new Audio.Sound(); 7 await crySound.loadAsync(opponent_pokemon.cry); 8 await crySound.playAsync(); 9 } catch (error) { 10 console.log("error loading cry: ", error); 11 } 12 }
The last thing we need to update is the battle screen. We also need to play the cry sound when the user receives an update that their opponent switched their Pokemon, or when their own Pokemon faints after receiving an attack.
In the code for handling the client-switched-pokemon
event, we update the anonymous function so it uses the async
keyword. Because we previously had a reference to the pokemon
, we can just use it to get the cry
:
1// src/screens/BattleScreen.js 2 3 my_channel.bind("client-switched-pokemon", async ({ team_member_id }) => { 4 // existing code here.. 5 6 // add these: 7 try { 8 let crySound = new Audio.Sound(); 9 await crySound.loadAsync(pokemon.cry); 10 await crySound.playAsync(); 11 } catch (error) { 12 console.log("error loading cry: ", error); 13 } 14 15 // this is existing code: 16 setTimeout(() => { 17 setMove("select-move"); 18 }, 1500); 19 });
Next, inside the handler for the client-pokemon-attacked
event, when the Pokemon faints, play the cry sound:
1if (data.health < 1) { // Pokemon faints 2 // existing code here.. 3 4 setTimeout(async () => { // note the async 5 // existing code here.. 6 7 // add these: 8 try { 9 let crySound = new Audio.Sound(); 10 await crySound.loadAsync(fainted_pokemon.cry); 11 await crySound.playAsync(); 12 } catch (error) { 13 console.log("error loading cry: ", error); 14 } 15 }, 1000); 16 17 // existing code here.. 18 }
Note that this time, we’ve placed the async
keyword in the function for setTimeout
instead of the event handler itself. This is because we only need it on the direct parent function.
Now it’s time to implement the animations. If you’re new to animations in React Native, I recommend that you check out my article on React Native animations.
Let’s first animate the health bar. Currently, when a Pokemon loses health, their current HP just abruptly changes when they receive the damage. We want to change it gradually so it gives the illusion that the Pokemon is slowly losing its health as it receives the attack:
To accommodate the animations, we first need to convert the HealthBar component to a class-based one. This is because we now need to work with the state:
1// src/components/HealthBar/HealthBar.js 2 3 class HealthBar extends Component { 4 render() { 5 const { label, currentHealth, totalHealth } = this.props; 6 7 // paste existing return code here.. 8 } 9 }
Next, extract the Animated
library from React Native. This allows us to perform animations:
import { View, Animated } from "react-native";
Next, declare the maximum width that the health bar can consume. We’ll be using this later to calculate the width to apply for the current health:
1import CustomText from "../CustomText"; 2 3 const available_width = 100; // add this
Next, initialize the state value which will represent the Pokemon’s current health. In the constructor, we also initialize the animated value. This is the value that we’ll interpolate so the health bar will be animated. Here, we’re using the currentHealth
passed via props so the health bar animations and health percentage text will always use the current Pokemon’s health:
1class HealthBar extends Component { 2 // add these: 3 state = { 4 currentHealth: this.props.currentHealth // represents the Pokemon's current health 5 }; 6 7 constructor(props) { 8 super(props); 9 this.currentHealth = new Animated.Value(this.props.currentHealth); // add this 10 } 11 12 // existing code here.. 13 14 }
You might be wondering why we need to add a separate state value for storing the Pokemon’s health when we’re already passing it as a prop. The answer is that we also want to animate the number which represents the health percentage while the health bar animation is in progress. The currentHealth
values passed via props only represents the current health, so we can’t really update it.
Next, add the getCurrentHealthStyles
function. This is where we define how the health bar will be updated while the animation is in progress. As you’ve seen in the demo earlier, the health bar should decrease its width and change its color from colors between green (healthy) to red (almost fainting). That’s exactly what we’re defining here:
1getCurrentHealthStyles = () => { 2 var animated_width = this.currentHealth.interpolate({ 3 inputRange: [0, 250, 500], 4 outputRange: [0, available_width / 2, available_width] 5 }); 6 7 const color_animation = this.currentHealth.interpolate({ 8 inputRange: [0, 250, 500], 9 outputRange: [ 10 "rgb(199, 45, 50)", 11 "rgb(224, 150, 39)", 12 "rgb(101, 203, 25)" 13 ] 14 }); 15 16 return { 17 width: animated_width, 18 height: 8, //height of the health bar 19 backgroundColor: color_animation 20 }; 21 };
In the code above, we’re using the interpolate
method to specify the input and output ranges of the animation. The inputRange
represents the value of the animated value at a given point in time, while the outputRange
is the value you want to use when the animated value is interpolated to the corresponding inputRange
. Here’s how the values for the animated_width
maps out. The number on the left is the inputRange
while the one in the right is the outputRange
:
The numbers in between the numbers we specified are automatically calculated as the animation in on progress.
The same idea applies to the values for color_animation
, only this time, it uses RGB color values as the outputRange
.
Next, update the render
method so it uses the Animated.View
component for the current health and call the getCurrentHealthStyles
function to apply the styles. The health percent text should also be updated to make use of the value in the state. It needs to be divided by 5 because the animated value is 5 times the value of the health bar’s available_width
:
1render() { 2 const { label } = this.props; 3 4 return ( 5 <View> 6 <CustomText styles={styles.label}>{label}</CustomText> 7 <View style={styles.container}> 8 <View style={styles.rail}> 9 <Animated.View style={[this.getCurrentHealthStyles()]} /> 10 </View> 11 <View style={styles.percent}> 12 <CustomText styles={styles.percentText}> 13 {parseInt(this.state.currentHealth / 5)}% 14 </CustomText> 15 </View> 16 </View> 17 </View> 18 ); 19 }
Lastly, add the componentDidUpdate
method. This gets invoked immediately after an update to the component occurs. The props don’t necessarily have to have been updated when this occurs, so we need to check whether the relevant prop was actually updated before we perform the animation. If it’s updated, we interpolate the this.currentHealth
animated value over a period of 1.5 seconds. The final value will be the new currentHealth
passed via props. After that, we add a listener to the animated value. This listener gets executed every time the animated value is updated. When that happens, we update the state value, which represents the Pokemon’s health. This allows us to update the UI with the current health percentage while the animation is in progress:
1componentDidUpdate(prevProps, prevState) { 2 if (prevProps.currentHealth !== this.props.currentHealth) { // check if health is updated 3 Animated.timing(this.currentHealth, { 4 duration: 1500, // 1.5 seconds 5 toValue: this.props.currentHealth // final health when the animation finishes 6 }).start(); // start the animation 7 8 this.currentHealth.addListener(progress => { 9 this.setState({ 10 currentHealth: progress.value 11 }); 12 }); 13 } 14 }
When a Pokemon loses all of its health, we move the PokemonFullSprite component downwards out of the view. This gives the impression that the Pokemon collapsed. Here’s what it looks like (minus the boxing gloves, we’ll add that later):
Just like what we did with all the previous components, we also need to convert this one to a class-based one.
Once you’ve converted the component to a class-based one, import the Animated
library:
1// src/components/PokemonFullSprite/PokemonFullSprite.js 2 3 import { Image, Animated } from "react-native";
Next, add the animated value that we’re going to interpolate:
1constructor(props) { 2 super(props); 3 this.sprite_translateY = new Animated.Value(0); 4 }
Next, update the render
method to specify how the vertical position of the component will change. In this case, an inputRange
of 0
means that it’s in its original position. Once it becomes 1000
, it’s no longer visible because its initial vertical position has moved 1000 pixels downwards. To apply the styles, specify it as an object under transform
. This allows us to perform translation animations similar to the ones used in CSS3:
1render() { 2 const { spriteFront, spriteBack, orientation } = this.props; 3 let sprite = orientation == "front" ? spriteFront : spriteBack; 4 5 // add these: 6 const pokemon_moveY = this.sprite_translateY.interpolate({ 7 inputRange: [0, 1], 8 outputRange: [0, 1000] 9 }); 10 11 // use Animated.Image instead of Image, and add transform styles 12 return ( 13 <Animated.Image 14 source={sprite} 15 resizeMode={"contain"} 16 style={[ 17 styles.image, 18 { 19 transform: [ 20 { 21 translateY: pokemon_moveY 22 } 23 ] 24 } 25 ]} 26 /> 27 ); 28 }
When the component is updated, we only start the animation if the Pokemon has fainted. If it’s not then we set the initial value. This way, the component doesn’t stay hidden if the user switched to a different Pokemon:
1componentDidUpdate(prevProps, prevState) { 2 if (prevProps.isAlive !== this.props.isAlive && !this.props.isAlive) { // if Pokemon has fainted 3 Animated.timing(this.sprite_translateY, { 4 duration: 900, 5 toValue: 1 6 }).start(); 7 } else if (prevProps.isAlive !== this.props.isAlive && this.props.isAlive) { // if Pokemon is alive 8 this.sprite_translateY.setValue(0); // unhides the component 9 } 10 }
The last step is to add the isAlive
prop when using the PokemonFullSprite component in the battle screen:
1// src/screens/BattleScreen.js 2 <PokemonFullSprite 3 ... 4 isAlive={opponent_pokemon.current_hp > 0} 5 /> 6 7 8 <PokemonFullSprite 9 ... 10 isAlive={pokemon.current_hp > 0} 11 />
When the user switches Pokemon, we’re going to make a Pokeball bounce and scale the Pokemon gif up. This gives the impression that the user has thrown it and the Pokemon came out of it:
To implement this animation, we also need to update the PokemonFullSprite component. Start by importing the additional components and libraries we need from React Native. This includes the View
component and the Easing
library to implement easing animations:
1// src/components/PokemonFullSprite/PokemonFullSprite.js 2 import { View, Image, Animated, Easing } from "react-native";
Next, update the constructor to include three new animated values. As mentioned earlier, we’re going to render a Pokeball which we will bounce so we need to translate its Y position. Aside from that, we also need to hide it so we have pokeball_opacity
. Once the Pokeball is hidden, we want to scale up the Pokemon gif:
1constructor(props) { 2 // previously added code.. 3 4 // add these 5 this.pokeball_y_translate = new Animated.Value(0); // for updating the Y position of the Pokeball 6 this.pokeball_opacity = new Animated.Value(0); // for animating the Pokeball opacity 7 this.sprite_scale = new Animated.Value(0); // for scaling the Pokemon gif 8 }
Next, update the render
method so it specifies how we’re going to interpolate the animated values we declared in the constructor:
1const pokemon_moveY = ... // same code 2 3 // add these: 4 const pokemon_scale = this.sprite_scale.interpolate({ 5 inputRange: [0, 0.5, 1], 6 outputRange: [0, 0.5, 1] // invisible (because zero size), half its original size, same as original size 7 }); 8 9 const pokeball_moveY = this.pokeball_y_translate.interpolate({ 10 inputRange: [0, 1, 2], 11 outputRange: [0, 50, 25] // top to bottom Y position translate 12 }); 13 14 const pokeball_opacity = this.pokeball_opacity.interpolate({ 15 inputRange: [0, 0.5, 1], 16 outputRange: [1, 0.5, 0] // full opacity, half opacity, invisible 17 });
Next, add an animated image on top of the Pokemon gif, then add the interpolated values to both the Pokeball image and the Pokemon gif. Since React Native doesn’t allow us to return siblings, we wrap everything in a View
component:
1return ( 2 <View> 3 <Animated.Image 4 source={require("../../assets/images/things/pokeball.png")} 5 style={{ 6 transform: [ 7 { 8 translateY: pokeball_moveY 9 } 10 ], 11 opacity: pokeball_opacity 12 }} 13 /> 14 15 <Animated.Image 16 source={sprite} 17 resizeMode={"contain"} 18 style={[ 19 styles.image, 20 { 21 transform: [ 22 { 23 translateY: pokemon_moveY 24 }, 25 { 26 scale: pokemon_scale 27 } 28 ] 29 } 30 ]} 31 /> 32 33 </View> 34 ); 35 }
You can get the Pokeball image from this website. Select the 32px .png
file. That’s also the source of the image included in the repo. Create a things
folder inside the src/assets/images
directory, move the file in there, and rename it to pokeball.png
.
Because we need to animate in two instances: componentDidMount
and componentDidUpdate
, we create a new function that will start the animations for us:
1animateSwitchPokemon = () => { 2 // initialize the animated values 3 this.sprite_translateY.setValue(0); 4 this.pokeball_opacity.setValue(0); 5 this.pokeball_y_translate.setValue(0); 6 this.sprite_scale.setValue(0); 7 8 // perform the animations in order 9 Animated.sequence([ 10 // bounce the Pokeball 11 Animated.timing(this.pokeball_y_translate, { 12 toValue: 1, 13 easing: Easing.bounce, 14 duration: 1000 15 }), 16 17 // hide the Pokeball 18 Animated.timing(this.pokeball_opacity, { 19 toValue: 1, 20 duration: 200, 21 easing: Easing.linear 22 }), 23 24 // scale the Pokemon gif up so it becomes visible 25 Animated.timing(this.sprite_scale, { 26 toValue: 1, 27 duration: 500 28 }) 29 ]).start(); 30 };
In the code above, we first re-initialize the animated values. This is because this component doesn’t really get unmounted when a Pokemon faints and re-mounted again once the user switches to another Pokemon. If we don’t do this, the subsequent Pokemon’s that we switch to after the first one has fainted will no longer be visible. That is because the component will already have been in its final state of animation.
Once we’ve re-initialized the animated values, we performed the animations in order:
When the component is mounted for the first time, we execute the function for animating it:
1componentDidMount() { 2 this.animateSwitchPokemon(); 3 }
Also, do the same when the component is updated. The only time we want to perform the animations for switching a Pokemon is when the user switches to a new one. Since we’re already passing the Pokemon name as a props, we simply check if the current one is not the same as the previous:
1componentDidUpdate(prevProps, prevState) { 2 if (prevProps.isAlive !== this.props.isAlive && !this.props.isAlive) { 3 // previous code here.. 4 } else if (prevProps.pokemon !== this.props.pokemon && this.props.isAlive) { 5 this.animateSwitchPokemon(); 6 } 7 }
Next, we’re going to implement the Pokemon move animations. We’ll only implement a single generic move animation because it would take us forever if we’re going to implement everything via code. Here’s what the animation looks like:
Just like all the previous Pokemon-related animations, we’ll also be using the PokemonFullSprite component for this. Start by adding the new animated values that were going to interpolate. This includes the following:
pokemon_opacity
- to seemingly make the Pokemon disappear for a split second to indicate that it received damage.punch_opacity
- for making the boxing gloves image appear while an attack is made, and disappear once it reaches its final destination (right above the Pokemon’s head).punch_translateY
- for moving the boxing gloves vertically across the target Pokemon when it’s attacked.Here’s the code. Add these after the last animated value in the constructor:
1// src/components/PokemonFullSprite/PokemonFullSprite.js 2 this.pokemon_opacity = new Animated.Value(0); 3 this.punch_opacity = new Animated.Value(0); 4 this.punch_translateY = new Animated.Value(0);
Next, we specify how the new animated values will be interpolated. This is inside the render
method:
1const pokeball_opacity = ... // same code 2 3 // add these: 4 const punch_opacity = this.punch_opacity.interpolate({ 5 inputRange: [0, 1], 6 outputRange: [0, 1] 7 }); 8 9 const punch_moveY = this.punch_translateY.interpolate({ 10 inputRange: [0, 1], 11 outputRange: [0, -130] // negative value because we're moving upwards 12 }); 13 14 const pokemon_opacity = this.pokemon_opacity.interpolate({ 15 inputRange: [0, 0.5, 1], 16 outputRange: [1, 0.2, 1] // appear, disappear, appear 17 });
Next, update the target components. The first one is the Pokemon gif. Add the opacity
style:
1<Animated.Image 2 source={sprite} 3 resizeMode={"contain"} 4 style={[ 5 styles.image, 6 { 7 transform: [ 8 { 9 translateY: pokemon_moveY 10 }, 11 { 12 scale: pokemon_scale 13 } 14 ], 15 opacity: pokemon_opacity // add this 16 } 17 ]} 18 />
The second one hasn’t been added yet. Add it right below the Pokemon gif. This includes both transform and opacity animations:
1<Animated.Image 2 source={require("../../assets/images/things/fist.png")} 3 style={[ 4 styles.punch, 5 { 6 transform: [ 7 { 8 translateY: punch_moveY // for moving it vertically across the Pokemon gif 9 } 10 ], 11 opacity: punch_opacity // for making it appear and disappear 12 } 13 ]} 14 />
You’ll need to download the image asset we’re using above. Select the 32px .png
file. That’s also the source of the image in the GitHub repo. Move the file inside the src/assets/images/things
folder and rename it to fist.png
.
Next, add the styles. The component should be absolutely positioned so that it can overlap with the Pokemon gif:
1const styles = { 2 // previously added code here.. 3 4 // add these: 5 punch: { 6 position: "absolute", // very important 7 bottom: -40, 8 left: 50 9 } 10 };
Next, add the function for starting the move animations:
1animateDamagePokemon = () => { 2 // reset the animated values 3 this.punch_opacity.setValue(0); 4 this.punch_translateY.setValue(0); 5 this.pokemon_opacity.setValue(0); 6 7 Animated.sequence([ 8 // make the boxing gloves visible 9 Animated.timing(this.punch_opacity, { 10 toValue: 1, 11 duration: 10, 12 easing: Easing.in 13 }), 14 15 // move the boxing gloves upwards across the Pokemon 16 Animated.timing(this.punch_translateY, { 17 toValue: 1, 18 duration: 300, 19 easing: Easing.in 20 }), 21 22 // hide the boxing gloves 23 Animated.timing(this.punch_opacity, { 24 toValue: 0, 25 duration: 200, 26 easing: Easing.in 27 }), 28 29 // momentarily hide the Pokemon (to indicate damage) 30 Animated.timing(this.pokemon_opacity, { 31 toValue: 1, 32 duration: 850, 33 easing: Easing.in 34 }) 35 ]).start(); 36 };
Next, we call the animateDamagePokemon
function when the current health changes. This may also happen when the user switches Pokemon so we need to make sure that the animation doesn’t execute if the previous Pokemon isn’t the same as the one the user switched to:
1componentDidUpdate(prevProps, prevState) { 2 // add these: 3 if ( 4 prevProps.pokemon === this.props.pokemon && 5 prevProps.currentHealth !== this.props.currentHealth 6 ) { 7 this.animateDamagePokemon(); 8 } 9 10 // existing code here.. 11 }
Next, when we use the PokemonFullSprite component inside the battle screen, we need to add the new currentHealth
prop. When its value changes, that’s the queue for the component to render the move animation:
1// src/screens/BattleScreen.js 2 <PokemonFullSprite 3 ... 4 currentHealth={opponent_pokemon.current_hp} 5 /> 6 7 <PokemonFullSprite 8 ... 9 currentHealth={pokemon.current_hp} 10 />
Lastly, we need to move the code for dispatching the action for updating the Pokemon’s health to the very first line when the callback function is called. This is because setting the Pokemon’s health triggers the move animation as well, and we want to perform it while the health is being animated:
1// src/screens/BattleScreen.js 2 my_channel.bind("client-pokemon-attacked", data => { 3 setPokemonHealth(data.team_member_id, data.health); // move this (previously above: setMove("select-move")) 4 5 // previously added code here.. 6 });
We’ve reached the end of this tutorial. Even though it took us three tutorials to implement it, there are still lots of things that need to be covered:
moves_data.js
file, but they’re not really implemented as such.You might also have noticed that there’s a bug in the app. I call it “Zombie mode”. When your Pokemon faints, you can actually go to the Pokemon move selection screen and attack with your fainted Pokemon.
Lastly, there’s no functionality yet to inform both players that someone has won. Even though it’s obvious, it’s always good to acknowledge it. So if you’re interested, I encourage you to develop the app further.
In this tutorial, you learned how to play background sounds within an app using Expo’s Audio API. You also learned how to implement animations in React Native.
That also wraps up the series. In this series, you learned how to re-create the battles in the classic Pokemon game using React Native and Pusher. Along the way, you learned how to use Redux, Pusher Channels, audio, and animations in React Native.
You can find the code for this app on its GitHub repo. The code added to this specific part of the series is on the animations-and-sounds
branch.