19

A Definitive React-Native Guide for React Developers: Part IV.

 5 years ago
source link: https://www.tuicool.com/articles/FBzQ7bI
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Welcome back! In this series, we’ll cover the basics of React-Native development and develop a mobile game together.

You can read the previous parts of this article series here if you missed any:

As of now, we have the main grid logic finished - but we don’t see our points, how much time we have left, we can’t lose, nor we can pause the game, and without these, our game couldn’t even be more pointless - so let’s implement these!

  • Building the Bottom Bar
    • How to position the bottom bar properly?
    • Importing the icons, writing some stylesheets
  • Making the layout a bit more flexible
    • Storing the game’s state (in game, lost, or paused) in the app state
    • Defining a proper pause button callback
    • Building the pause/lost screen
    • Making the user able to exit back to the home screen

Building the Bottom Bar

AV3MFfN.png!web

As you can see, the bottom bar plays a main role in informing the user about their progress in the game: it shows their points, their time left, the bests they have done yet, and they can pause the game from there. Let’s build it already!

Let’s start with the container: create the style, name it as bottomContainer (or anything you’d like to), and to make sure that the grid and the bottom bar have the same width, let’s start with width: Dimensions.get(“window”).width * 0.875 .

We want to make sure that our app looks nice on every screen size, thus we have to initialize flex as a method to make our layout responsible, too: let’s bring flex: 1 to the party. Last but not least, to display the views inside the container next to each other, give it a flexDirection: ‘row’ property. When finished, your stylesheet should look like this:

bottomContainer: {
   flex: 1,
   width: Dimensions.get("window").width * 0.875,
   flexDirection: 'row'
}

(Don’t forget to import Dimensions from react-native among with StyleSheets !)

Next off, add a <View> , pass it our bottomContainer stylesheet as a prop, and add 3 empty view s with flex: 1 as a child.

They will contain the three main parts of the bar:

<View style={styles.bottomContainer}>
 <View style={{ flex: 1 }}>
 </View>
 <View style={{ flex: 1 }}>
 </View>
 <View style={{ flex: 1 }}>
 </View>
</View>

When you save and reload the app, you won’t really notice anything: that’s because there’s nothing in these containers yet! So let’s start with the Points and the Time counters, then move on to the “best yet” labels, and finish off with the pause button.

To make sure that both of the counters have the same style, create one style and re-use them on both the counters:

counterCount: {
  fontFamily: 'dogbyte',
  textAlign: 'center',
  color: '#eee',
  fontSize: 50
},
counterLabel: {
  fontFamily: 'dogbyte',
  textAlign: 'center',
  color: '#bbb',
  fontSize: 20
}

You can simply add some <Text> s to display the values that are in the state:

<View style={{ flex: 1 }}>
 <Text style={styles.counterCount}>
   {this.state.points}
 </Text>
 <Text style={styles.counterLabel}>
   points
 </Text>
</View>
(...)
<View style={{ flex: 1 }}>
 <Text style={styles.counterCount}>
   {this.state.timeLeft}
 </Text>
 <Text style={styles.counterLabel}>
   seconds left
 </Text>
</View>

If you save and refresh your app, you’ll see something like this:

qAbam2B.png!web

If you wait for long enough without touching the correct tile, you’ll see the timer going negative. We’ll fix that later. Let’s continue with the “best yet” labels. You can build them yourself, but if you get stuck, continue reading the guide. (If you think you created a suitable solution, feel free to skip to the “Pausing the game” section)

First, create a container that will have the icon and the text as children in it:

bestContainer: {
  marginTop: 10,
  flexDirection: 'row',
  justifyContent: 'center'
}

This snippet will look differently in the current state of the git repo as I added this after initially writing the article - but you can check the correct code on the latest tree , too.

The styling is pretty self-explanatory there. The icon and the label also have a really straightforward styling:

bestIcon: {
  width: 25,
  height: 25,
  marginRight: 5
},
bestLabel: {
  fontFamily: 'dogbyte',
  color: '#bbb',
  fontSize: 25,
  marginTop: 2.5,
}

We can use hardcoded values for now as the value of the <Text> - we’ll make these display the correct value later in the “Persisting data” section.

You can import the trophy icon from the assets as you can see below. (For the “longest time” counter, the icons/clock.png is the suitable icon.)

<View style={styles.bestContainer}>
    <Image source={require('../../assets/icons/trophy.png')} style={styles.bestIcon} />
    <Text style={styles.bestLabel}>
       0
    </Text>
</View>

And if you reload the app, you’ll see the bottom bar with the icons:

6BziYrI.png!web

Making the Layout a Bit more Flexible

If you are not developing on an iPhone X/s/r, you may have already noticed that this layout looks weird.

When defining the bottomContainer style, I already mentioned making our app responsible and flexible. We’ll also cover this in a later section of the course, but the layout needs a fix ASAP.

You can simply fix it by adding a container for the Header with the style properties flex: 1, justifyContent: "center" , then wrapping the grid in a supplementary/spacing buffer container, too, with the style properties flex: 5, justifyContent: "center" . Then, add a container for the bottom bar and give it flex: 2 as a property. Inside the bottom bar, you have three views with only the flex: 1 property as a style.

Outsource the styles from the bottom bar section’s container to the styles.js as it follows:

bottomSectionContainer: {
 flex: 1,
 marginTop: 'auto',
 marginBottom: 'auto'
},

This will vertically center the views inside the container and make sure that they fill up their space. Then, use this style in all three views. The game screen’s layout will now look great on all devices.

This whole “Making the layout a bit more flexible” section will be missing from the current state of the git repo as I added this after initially writing the article -

but you can check the code on the latest tree if you need to.

Pausing our React-Native Game

Before just adding the pause button icon to a TouchableOpacity , we need to think about a way to prevent the player from cheating with the pause feature: if we just pause the game without hiding the grid, the players can easily search for the differing tile, continue the game and repeat this for the end of the time.

So instead, we should spice things up with some conditions. For the initial state, add another property:

gameState: 'INGAME' // three possible states: 'INGAME', 'PAUSED' and 'LOST'

Then, inside the componentWillMount() ’s setInterval , add a ternary operator to make sure that the timer doesn’t get modified while the game is paused:

this.state.gameState === 'INGAME' && this.setState({ timeLeft: this.state.timeLeft - 1 });

Then wrap the grid generator in a ternary operator, too - so that we can hide the grid when the game is paused.

{gameState === 'INGAME' ? ? Array(size) (...)
 )) : (
    <View style={styles.pausedContainer}>
       <Image
         source={require("../../assets/icons/mug.png")}
         style={styles.pausedIcon}
       />
      <Text style={styles.pausedText}>COVFEFE BREAK</Text>
    </View>
 )
}

Oh, and create the styles needed for the pause-related elements, too:

pausedContainer: {
  flex: 1,
  alignItems: 'center',
  justifyContent: 'center'
},
pausedText: {
  fontFamily: 'dogbyte',
  textAlign: 'center',
  color: '#eee',
  marginTop: 20,
  fontSize: 60,
},
pausedIcon: {
  width: 80,
  height: 80
}

And finally, add the pause/play/replay button to the bottom bar. To decide which icon we need to import, I used a ternary operator, but you can use if statements if that’s what you prefer:

const bottomIcon =
     gameState === "INGAME"
       ? require("../../assets/icons/pause.png")
       : gameState === "PAUSED"
       	? require("../../assets/icons/play.png")
      : require("../../assets/icons/replay.png");

And in the JSX, add the code below to the second child of the bottomContainer. This code uses the bottomIcon constant we declared the previous snippet as a source, and uses the bottomIcon style from the stylesheet. Keep an eye on not mixing them up!

<TouchableOpacity style={{ alignItems: 'center' }} onPress={this.onBottomBarPress}>
<Image source={bottomIcon} style={styles.bottomIcon} />
</TouchableOpacity>

The bottomIcon style is just a simple width: 50; height: 50 , I think you can do it yourself by now.

Now, let’s add the event handler for the bottom button:

onBottomBarPress = async () => {
   switch(this.state.gameState) {
     case 'INGAME': {
       this.setState({ gameState: 'PAUSED' });
       break;
     }
     case 'PAUSED': {
       this.setState({ gameState: 'INGAME' });
       break;
     }
     case 'LOST': {
       await this.setState({ points: 0, timeLeft: 15, size: 2 });
       this.generateNewRound();
       this.setState({
         gameState: "INGAME",
       })
       break;
     }
   }
 };

This will pause the game if you are in a game, resume the game if you paused, and restart the game if you have lost.

(The losing case may seem a bit odd: first, I reset the state to the original except the gameState so that the grid does not get rendered yet. I need to await the setState to make sure that the generation happens only with the new state - if not, the game could accidentally create a winner tile out of the grid (with a coordinate like [3, 2] on a 2-by-2 grid) and you’d have no possibility but to lose. After the new round’s data is generated, the gameState is updated so that you can see the grid again.)

If you save and reload the app, you’ll see the icon, and if you tap on it, you’ll be able to pause the game and resume it - but you still can’t lose yet, so let’s implement that.

Inside the componentWillMount() , add an if operator that will decide whether you used or not:

this.interval = setInterval(() => {
     if (this.state.gameState === "INGAME") {
       if (this.state.timeLeft <= 0) {
         this.setState({ gameState: "LOST" });
       } else {
         this.setState({ timeLeft: this.state.timeLeft - 1 });
       }
     }
 }, 1000);

And since you can lose, you need a screen for that, too. You can add another ternary to achieve this:

gameState === "INGAME" ? (
   Array(size) (...)
) : gameState === "PAUSED" ? (
   <View style={styles.pausedContainer}>
       <Image
         source={require("../../assets/icons/mug.png")}
         style={styles.pausedIcon}
       />
          <Text style={styles.pausedText}>COVFEFE BREAK</Text>
       </View>
) : (
   <View style={styles.pausedContainer}>
       <Image
          source={require("../../assets/icons/dead.png")}
          style={styles.pausedIcon}
       />
       <Text style={styles.pausedText}>U DED</Text>
   </View>
)

Please keep in mind that using a lot of ternary operators may make your code unreadable, and you should try to avoid overusing them.

If you run the game now, you can properly pause, continue, lose and replay the game. Nice job! :raised_hands::clap:

Just one more thing before finishing with this section: you can’t quit to the main menu! *

(I noticed this after initially writing this article, so in the current state of the GitHub repo tree, you are not going to be able to see it - but you be able to see it on the latest tree , though)*

Let’s just add a simple button to fix that:

) : (
 <View style={styles.pausedContainer}>
   {gameState === "PAUSED" ? (
     <Fragment>
       <Image
         source={require("../../assets/icons/mug.png")}
         style={styles.pausedIcon}
       />
       <Text style={styles.pausedText}>COVFEFE BREAK</Text>
     </Fragment>
   ) : (
     <Fragment>
       <Image
         source={require("../../assets/icons/dead.png")}
         style={styles.pausedIcon}
       />
       <Text style={styles.pausedText}>U DED</Text>
     </Fragment>
   )}
   <TouchableOpacity onPress={this.onExitPress}>
     <Image
       source={require("../../assets/icons/escape.png")}
       style={styles.exitIcon}
     />
   </TouchableOpacity>
 </View>
)}

Notice how I didn’t copy-paste the TouchableOpacity again and again: since we need to show it on all screens, we can prevent copy-pasting by adding a container with the pausedContainer style on, then changing the original View s with the pausedContainer style to Fragment s.

If using Fragments is a new concept for you (it may be because it’s a relatively new concept in React), be sure to check out the docs . In a nutshell, you can wrap your components with them without adding a new div to the DOM.

Let’s add the event handler for the exit button, too:

onExitPress = () => {
 this.props.navigation.goBack();
};

Now we’re all set. You can go back to the home screen with a shiny new button.

If you want to check out the code that’s finished as of now in one piece, here’s the GitHub repo .

You totally deserve a pat on the back for making it this far in the series, so kudos on that! :facepunch: In the next section, we’ll continue with animations, music, and SFX.

You can reread the previous articles here:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK