Create a Pokemon battle game with React Native - Part 3: Animations and sounds

Introduction

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:

Prerequisites

This tutorial has the same prerequisites as part two of the series.

Overview of features to add

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:

  • Screen
  • Pokemon cry

Here are the animations that we’re going to implement:

  • Health bar
  • Pokemon moves

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.

Screen sounds

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:

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.

Login screen background sound

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    };

Team selection screen background sound

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    }

Battle screen background sound

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    }

Pokemon cry sounds

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.

Update the PokemonOption component

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    }

Update the MovesList component

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    }

Update the battle screen

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.

Health bar animation

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:

rn-pokemon-3-1

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:

  • 0 → 0
  • 250 → 50
  • 500 → 100

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    }

Pokemon fainting animation

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):

rn-pokemon-3-2

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    />

Pokemon switch animation

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:

rn-pokemon-3-3

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:

  • Bounce the Pokeball.
  • Hide the Pokeball.
  • Scale the Pokemon gif up.

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    }

Pokemon move animation

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:

rn-pokemon-3-4

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    });

Conclusion

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:

  • Only a handful of Pokemon can be selected, and their moves are limited.
  • There are two-turn moves like Fly, Dig, and Solar Beam in the moves_data.js file, but they’re not really implemented as such.
  • Moves that are supposed to modify base stats aren’t also implemented. There are also no status conditions such as frozen, burned, or paralyzed.
  • Players can’t use or equip items such as the max potion, berries to their Pokemon.

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.