Create a Pokemon battle game with React Native - Part 2: Two-player mode
You will need Node 8+, Expo and Yarn installed on your machine.
In this tutorial, we’ll be implementing the game’s two-player mode.
If you need an overview of what the final output for this series will look like, or if you need more details on what we’ll be doing in each part, check out part one.
This is the second tutorial of a three-part series on creating Pokemon battle game with React Native.
These are the topics covered in this series:
- Part one: Practice mode
- Part two: Two-player mode
- Part three: Animations and sounds
Prerequisites
This tutorial has the same prerequisites as part one of the series.
We’ll be using Pusher to communicate data between players in realtime so you need to create a Pusher account and an app instance. We’ll be specifically using Pusher package version 4.3.1 for the app.
Overview of features to add
We’ve added a lot of code in part one. This time, we’re mostly going to update the existing code in order for us to connect to Pusher and be able to emit moves made by each player, so their opponent’s screen also gets updated with the same changes. In this case, we need to implement the following:
- When a user switches Pokemon, emit an event via Pusher Channels so their opponent also sees the new Pokemon they switched to.
- When user attacks with their Pokemon, the opponent’s Pokemon should also receive the damage on their side.
Here’s what the final output for this part is going to look like:
Enable client events
On your Pusher app instance, enable client events in the settings page. This allows us to trigger client events right from the app itself. A server will still be required for authentication though:
Add the two-player mode
At this point, we’re ready to start adding the code for the two-player mode. Start by installing the Pusher JavaScript library. This allows us to use Channels:
yarn add pusher-js
Update the action types
Update the action types file to include the new actions that we will be working with:
// src/actions/types.js
export const SET_POKEMON_HEALTH = "set_pokemon_health";
export const SET_MESSAGE = "set_message";
export const REMOVE_POKEMON_FROM_TEAM = "remove_pokemon_from_team";
Since the two-player mode is already going to involve two players, we need a way to also update the health of the current player’s Pokemon (SET_POKEMON_HEALTH
), as well as remove them from the team when they faint (REMOVE_POKEMON_FROM_TEAM
). The SET_MESSAGE
action is for informing both players on what each other’s actions are. For example: “opposing player switched to Pikachu”.
Next, add the action creators for the action types we just added:
// src/actions/index.js
import {
// existing code here..
// add these:
SET_POKEMON_HEALTH,
SET_MESSAGE,
REMOVE_POKEMON_FROM_TEAM
} from "./types";
// add these after the last function
export const setPokemonHealth = (team_member_id, health) => {
return {
type: SET_POKEMON_HEALTH,
team_member_id, // the unique ID assigned to a specific Pokemon on a team
health
};
};
export const setMessage = message => {
return {
type: SET_MESSAGE,
message // the message to display on the player's screen
};
};
export const removePokemonFromTeam = team_member_id => {
return {
type: REMOVE_POKEMON_FROM_TEAM,
team_member_id
};
};
Update the battle reducer
Update the battle reducer so it can process the actions we created. Start by including the action types:
// src/reducers/BattleReducer.js
import {
// existing code here..
// add these:
SET_POKEMON_HEALTH,
SET_MESSAGE,
REMOVE_POKEMON_FROM_TEAM
} from "../actions/types";
Next, we add wait-for-turn
as a move display text. In part one, we already added three of the move display text. These are the text displayed on top of the actual controls (fight or switch, Pokemon move selection, and Pokemon selection). It basically guides the user on what to do with the controls. wait-for-turn
is empty because we don’t want to display a move display text while the user is waiting for their turn:
const move_display_text = {
"wait-for-turn": "", // add this
// existing code here..
};
Next, add the default message to display. This message is different from the move display text. Its primary function is to inform the user what move their opponent did, and it also shows them what move they did and how effective it is:
const INITIAL_STATE = {
// existing code here..
message: "" // add this
};
Next, add the conditions that will process the actions:
case SET_POKEMON_HEALTH: // updates the current_hp of the Pokemon with the team_member_id specified in the action
let team_data = [...state.team];
team_data = team_data.map(item => {
if (item.team_member_id == action.team_member_id) {
item.current_hp = action.health;
}
return item;
});
return { ...state, team: team_data };
case SET_MESSAGE: // sets the message to display in place of the controls
return { ...state, move: "wait-for-turn", message: action.message };
case REMOVE_POKEMON_FROM_TEAM: // removes the Pokemon with the specified team_member_id from the team
const diminished_team = [...state.team].filter(item => {
return item.team_member_id != action.team_member_id;
});
return { ...state, team: diminished_team };
Update the team selection screen
At this point, we’re now ready to update the team selection screen so it actually looks for an opponent instead of emulating that it’s looking for an opponent. Start by including the components and libraries we need:
import { View, TouchableOpacity, ActivityIndicator, Alert } from "react-native"; // add Alert
import Pusher from "pusher-js/react-native";
Next, update the constructor so it declares an initial value for the Pusher client reference and the current user’s channel. This channel is where the opponent triggers events for updating their opponent Pokemon’s health and informing their opponent that they switched Pokemon:
constructor(props) {
// existing code here..
// add these:
this.pusher = null;
this.my_channel = null;
}
Next, update the function that gets executed when the user confirms their team selection to store the pokemon_ids
and team_member_ids
in a separate array. We will use those later as additional information when we authenticate the user with the server component of the app:
confirmTeam = () => {
const { selected_pokemon, setTeam, setPokemon, navigation } = this.props;
let team = [...selected_pokemon];
let pokemon_ids = []; // add this
let team_member_ids = []; // add this
team = team.map(item => {
let hp = 500;
let shuffled_moves = shuffleArray(item.moves);
let selected_moves = shuffled_moves.slice(0, 4);
let moves = moves_data.filter(item => {
return selected_moves.indexOf(item.id) !== -1;
});
let member_id = uniqid();
pokemon_ids.push(item.id); // add this
team_member_ids.push(member_id); // add this
return {
...item,
team_member_id: member_id,
current_hp: hp,
total_hp: hp,
moves: moves,
is_selected: false
};
});
setTeam(team);
setPokemon(team[0]);
this.setState({
is_loading: true
});
// next: add code for authenticating with the server
};
Next, add the code for authenticating the user to the server. Here, we’re passing the username
, pokemon_ids
and team_member_ids
as additional params. These are used later on so that both users have a copy of their opponents team. The users won’t really know all the Pokemon that are in their opponent’s team, only the app needs to know about that information so it can update the health and remove the Pokemon from the opponent’s team when they faint:
const username = navigation.getParam("username"); // get the username passed from the login screen
this.pusher = new Pusher("YOUR_PUSHER_APP_KEY", {
authEndpoint: "YOUR_NGROK_URL/pusher/auth",
cluster: "YOUR_PUSHER_APP_CLUSTER",
encrypted: true,
auth: {
params: {
username: username,
pokemon_ids: pokemon_ids,
team_member_ids: team_member_ids
}
}
});
// next: subscribe to current user's Pusher channel
In the above code, replace the placeholder values with your Pusher credentials. We’ll replace the ngrok URL later once we get to the server part.
Next, subscribe to the current user’s Pusher channel. If the subscription succeeds, we listen for the opponent-found
event to be triggered. This event is triggered by the server once it finds an opponent for the user. As you’ll see later, the server will send the usernames of the two users that were matched. That’s what we’re picking up when the opponent-found
event is triggered.
We determine the opponent by comparing the username of the user to the usernames sent from the server. The first turn goes to the user who first confirmed their team selection. Lastly, we send all the relevant information to the next screen by means of navigation props:
this.my_channel = this.pusher.subscribe(`private-user-${username}`);
this.my_channel.bind("pusher:subscription_error", status => {
Alert.alert(
"Error",
"Subscription error occurred. Please restart the app"
);
});
this.my_channel.bind("pusher:subscription_succeeded", data => {
this.my_channel.bind("opponent-found", data => {
let opponent =
username == data.player_one.username
? data.player_two // object containing player two's data
: data.player_one; // object containing player one's data
let first_turn =
username == data.player_one.username
? "you"
: data.player_two.username;
Alert.alert(
"Opponent found!",
`${
opponent.username
} will take you on! First turn goes to ${first_turn}`
);
this.setState({
is_loading: false,
username: ""
});
// send all relevant information to the next screen
navigation.navigate("Battle", {
pusher: this.pusher,
username: username,
opponent: opponent,
my_channel: this.my_channel,
first_turn: first_turn
});
});
});
Update the battle screen
We’re now ready to update the battle screen so it can handle the two-player mode. Start by importing the new action creators we added earlier:
// src/screens/BattleScreen.js
import {
// existing code here..
// add these:
setPokemonHealth,
removePokemonFromTeam,
setMessage,
removePokemonFromOpponentTeam
} from "../actions";
Next, update mapStateToProps
to include the message
from the store. This way, the battle screen’s UI will always stay up to date with the current value of message
in the store:
const mapStateToProps = ({ team_selection, battle }) => {
const {
// existing code here..
message // add this
} = battle;
return {
// existing code here..
message // add this
};
};
Next, add the functions for dispatching the new actions in mapDispatchToProps
:
const mapDispatchToProps = dispatch => {
return {
// existing code here..
// add these:
setMessage: message => {
dispatch(setMessage(message));
},
setPokemonHealth: (team_member_id, health) => {
dispatch(setPokemonHealth(team_member_id, health));
},
setMove: move => {
dispatch(setMove(move));
},
removePokemonFromTeam: team_member_id => {
dispatch(removePokemonFromTeam(team_member_id));
},
removePokemonFromOpposingTeam: team_member_id => {
dispatch(removePokemonFromOpponentTeam(team_member_id));
}
};
};
From the code above, you can see that it’s not just the new actions we’re adding. We also have previously added actions that we didn’t have previously. This includes setMove
and removePokemonFromOpponentTeam
. Previously, we didn’t need to add those because we’re only dispatching them from the MovesList
component. This time, we need to add them to the screen itself because it is where we will be putting all of the event listeners for Pusher Channels.
Next, update the constructor
to add an initial value for the opponents_channel
. We will be using this channel to inform the user’s opponent when their current Pokemon receives damage. We also use it for sending messages to display on the opponent’s control section:
constructor(props) {
super(props);
this.opponents_channel = null;
}
Next, extract all of the store values and functions we returned earlier from mapDispatchToProps
:
async componentDidMount() {
const {
// existing code here..
// add these:
navigation,
team,
setMove,
removePokemonFromOpposingTeam,
setMessage,
setPokemonHealth,
removePokemonFromTeam
} = this.props;
// next: construct opponent team data
}
Next, construct the opponent team data based on the pokemon_ids
and team_member_ids
that were passed from the team selection screen earlier:
let pusher = navigation.getParam("pusher");
const { username, pokemon_ids, team_member_ids } = navigation.getParam(
"opponent"
);
let opponent_pokemon_ids = pokemon_ids.split(",");
let opponent_team_member_ids = team_member_ids.split(",");
// only return the data of the Pokemon's that are on the opponent's team
let opponent_team_data = pokemon_data.filter(item => {
return opponent_pokemon_ids.indexOf(item.id.toString()) !== -1;
});
opponent_team_data = opponent_team_data.map((item, index) => {
let hp = 500;
let shuffled_moves = shuffleArray(item.moves);
let selected_moves = shuffled_moves.slice(0, 4);
let moves = moves_data.filter(item => {
return selected_moves.indexOf(item.id) !== -1;
});
return {
...item,
current_hp: hp,
total_hp: hp,
moves: moves,
is_selected: false
};
});
Once we have the opponent team data, we need to sort it based on the ordering of Pokemon the opponent has used when they were selecting their team. This ordering is represented by how the items in the opponent_pokemon_ids
array are arranged so we loop through that array, and add the team member ID to each Pokemon. We then save the sorted opponent Pokemon team in the store:
let sorted_opponent_team = [];
opponent_pokemon_ids.forEach((id, index) => {
let team_member = opponent_team_data.find(
item => id == item.id.toString()
);
team_member.team_member_id = opponent_team_member_ids[index];
sorted_opponent_team.push(team_member);
});
// save the opponent Pokemon team in the store
setOpponentTeam(sorted_opponent_team);
setOpponentPokemon(sorted_opponent_team[0]);
// next: subscribe to opponent's channel
Next, subscribe to the opponent’s channel. Once subscribed, get the username of the user who will make the first move, and if it’s not the current user, call setMove
with wait-for-turn
as the argument. This effectively locks the user’s controls so they can no longer perform any actions while their opponent hasn’t made their move yet:
this.opponents_channel = pusher.subscribe(`private-user-${username}`);
this.opponents_channel.bind("pusher:subscription_error", status => {
Alert.alert(
"Error",
"Subscription error occurred. Please restart the app"
);
});
this.opponents_channel.bind("pusher:subscription_succeeded", data => {
const first_turn = navigation.getParam("first_turn");
if (first_turn != "you") {
setMessage("Please wait for you turn..."); // set message to display in place of the controls UI
setMove("wait-for-turn");
}
});
// next: listen for the event when the opponent informs the user that they switched Pokemon
Next, listen for the event when the opponent informs the user that they switched Pokemon. This event includes the team_member_id
of the opponent Pokemon as its data. We use that ID to get the Pokemon data object from the sorted_opponent_team
from earlier. From there, we just set the message to inform the user which Pokemon their opponent used, and then change the current opponent Pokemon by calling the setOpponentPokemon
function. As setting a message automatically locks the UI, we need to call the setMove
function after 1.5 seconds so the user can also make their move:
let my_channel = navigation.getParam("my_channel");
my_channel.bind("client-switched-pokemon", ({ team_member_id }) => {
let pokemon = sorted_opponent_team.find(item => {
return item.team_member_id == team_member_id;
});
setMessage(`Opponent changed Pokemon to ${pokemon.label}`);
setOpponentPokemon(pokemon);
setTimeout(() => {
setMove("select-move");
}, 1500);
});
// next: listen for event when the user's Pokemon is attacked
Next, listen for the event when the user’s Pokemon is attacked. This event includes the updated health
, team_member_id
of the Pokemon, and the message
to display. We use those data to update the UI. If the Pokemon’s health goes below 1, we get the data of that Pokemon and set its health to zero and remove it from the team. This is because, most likely, the health will become a negative value, which will make the health bar all red. Setting it to zero will make it white instead:
my_channel.bind("client-pokemon-attacked", data => {
setMessage(data.message);
// update the UI with the new health and allow user to make a move after 1.5 seconds
setTimeout(() => {
setPokemonHealth(data.team_member_id, data.health);
setMove("select-move");
}, 1500);
if (data.health < 1) { // if the Pokemon faints
let fainted_pokemon = team.find(item => {
return item.team_member_id == data.team_member_id;
});
setTimeout(() => {
setPokemonHealth(data.team_member_id, 0);
setMessage(`${fainted_pokemon.label} fainted`);
removePokemonFromTeam(data.team_member_id);
}, 1000);
// let the user select the Pokemon to switch to
setTimeout(() => {
setMove("select-pokemon");
}, 2000);
}
});
Next, update the render
method so it displays the current message
value. Also, add the conditions to selectively display the move_display_text
, we don’t really need to display it if message
is not empty. The PokemonList
and MovesList
also shouldn’t be displayed until the opponents_channel
is initialized because we’re passing it to those components. Note that the …
indicates that the same props are used, so you simply have to copy the new props. In this case, the only new props are the opponents_channel
for the PokemonList
and MovesList
components:
render() {
const {
team,
move,
move_display_text,
pokemon,
opponent_pokemon,
backToMove,
message // add this
} = this.props;
return (
<View style={styles.container}>
<CustomText styles={[styles.headerText]}>Fight!</CustomText>
<View style={styles.battleGround}>
{opponent_pokemon && (
<View style={styles.opponent}>
<HealthBar ... />
<PokemonFullSprite ... />
</View>
)}
{pokemon && (
<View style={styles.currentPlayer}>
<HealthBar ... />
<PokemonFullSprite ... />
</View>
)}
</View>
<View style={styles.controls}>
<View style={styles.controlsHeader}>
{(move == "select-pokemon" || move == "select-pokemon-move") && (
<TouchableOpacity
style={styles.backButton}
onPress={() => {
backToMove();
}}
>
<Ionicons name="md-arrow-round-back" size={20} color="#333" />
</TouchableOpacity>
)}
{move != "wait-for-turn" && (
<CustomText styles={styles.controlsHeaderText}>
{move_display_text}
</CustomText>
)}
{move == "wait-for-turn" && (
<CustomText styles={styles.message}>{message}</CustomText>
)}
</View>
{move == "select-move" && <ActionList />}
{move == "select-pokemon" &&
this.opponents_channel && (
<PokemonList
...
opponents_channel={this.opponents_channel}
/>
)}
{pokemon &&
this.opponents_channel &&
move == "select-pokemon-move" && (
<MovesList
...
opponents_channel={this.opponents_channel}
/>
)}
</View>
</View>
);
}
Update the PokemonList component
Next, we need to make the opponents_channel
props available to the PokemonOption
component. Note that we could have used the React Context API or Redux for this. But to simplify things, we’re just going to “drill it down” to the component which actually needs it:
// src/components/PokemonList/PokemonList.js
const PokemonList = ({
// existing code here..
opponents_channel // add this
}) => {
// existing code here..
});
Look for the return statement, and pass the value of opponents_channel
to PokemonOption
:
<PokemonOption
// existing code here..
opponents_channel={opponents_channel} // add this
/>
Update the PokemonOption component
If you still remember, the PokemonOption
component is used for two things: for selecting Pokemon to be included in the team, and for selecting a Pokemon to switch to. The updates that we’re going to do is only for the latter, so the code in the condition for selecting a Pokemon for a team should stay intact.
Start by importing the actions that we need:
// src/components/PokemonOption/PokemonOption.js
import { selectPokemon, setPokemon, setMove, setMessage } from "../../actions";
Next, update the mapDispatchToProps
function to expose the functions for dispatching the setMessage
and setMove
actions to the component:
const mapDispatchToProps = dispatch => {
return {
// existing code here..
// add these:
setMessage: message => {
dispatch(setMessage(message));
},
setMove: move => {
dispatch(setMove(move));
}
};
};
Scroll to the top, and extract the functions you just exposed. Don’t forget to include the opponents_channel
as well:
const PokemonOption = ({
// existing code here..
// add these:
setMessage,
setMove,
opponents_channel
}) => {
// existing code here..
});
Next, update the switch-pokemon
condition so it updates the message displayed in the controls section, and sets the user’s current Pokemon to the selected one. After that, inform the opponent by triggering the client-switched-pokemon
event on their channel. As you’ve seen earlier, this would allow the opponent to make a move. Thus, we need to set the user to wait for their turn:
if (action_type == "select-pokemon") {
// existing code here..
} else if (action_type == "switch-pokemon") {
// replace existing code with these:
setMessage(`You used ${pokemon_data.label}`);
setPokemon(pokemon_data);
opponents_channel.trigger("client-switched-pokemon", {
team_member_id: pokemon_data.team_member_id // the ID of the Pokemon the user switched to
});
setTimeout(() => {
setMessage("Please wait for your turn...");
setMove("wait-for-turn");
}, 2000);
}
Update the MovesList component
The final thing we need to implement before we move on to the server component is the updating of the opponent Pokemon’s health.
Start by importing the setMessage
action:
// src/components/MovesList/MovesList.js
import {
// existing code here..
setMessage // add this
} from "../../actions";
Next, update the mapStateToProps
function to include the data on the user’s current Pokemon. This allows us to inform the opponent on which of their opponent’s Pokemon has made the move:
const mapStateToProps = ({ battle }) => {
const { opponent_pokemon, pokemon } = battle;
return {
opponent_pokemon,
pokemon // add this
};
};
Next, update mapDispatchToProps
to expose functions for dispatching the setMove
and setMessage
actions:
const mapDispatchToProps = dispatch => {
// existing code here..
// add these:
backToMove: () => {
dispatch(setMove("select-move"));
},
setMessage: message => {
dispatch(setMessage(message));
}
}
Next, extract the new data and functions we’ve mapped to this component’s props:
const MovesList = ({
// existing code here..
// add these:
pokemon,
opponents_channel,
backToMove,
setMessage
}) => {
// existing code here..
})
Lastly, update the onPress
function to construct the message to be displayed on the user’s controls UI. This includes the name of the Pokemon, the name of the move, and its effectiveness. After that, inform the opponent that their Pokemon was attacked:
let { effectiveness, damage } = getMoveEffectivenessAndDamage(
item,
opponent_pokemon
); // extract effectiveness
let health = opponent_pokemon.current_hp - damage;
// add these:
let message = `${pokemon.label} used ${
item.title
}! ${effectiveness}`;
setMessage(message);
// inform the opponent that their Pokemon was attacked
opponents_channel.trigger("client-pokemon-attacked", {
team_member_id: opponent_pokemon.team_member_id,
message: message, // so the opponent sees the same message displayed on this user's screen
health: health
});
setOpponentPokemonHealth(opponent_pokemon.team_member_id, health);
if (health < 1) {
setOpponentPokemonHealth(opponent_pokemon.team_member_id, 0); // set health to zero so health bar is not all red
removePokemonFromOpponentTeam(opponent_pokemon.team_member_id);
}
setTimeout(() => {
setMessage("Please wait for your turn...");
setMove("wait-for-turn");
}, 1500);
Add the server code
At this point, we’re ready to add the server code. Create a server
folder inside the root of your project directory, navigate inside it, and execute npm init
. Just answer the questions by entering a blank value.
Next, open the generated package.json
file and change the value of name
to RNPokeBattle-server
.
After that, install all the packages we need:
npm install body-parser dotenv express pusher
Next, create a server.js
file and add the following. This includes the packages we just installed and initializes them:
var express = require("express"); // for setting up a server
var bodyParser = require("body-parser");
var Pusher = require("pusher"); // for connecting to Pusher
var app = express();
app.use(bodyParser.json()); // parse request body to JSON format
app.use(bodyParser.urlencoded({ extended: false })); // allow parsing of URL encoded request body
require("dotenv").config(); // load environment variables from .env file
Next, initialize the variable where we’ll store the users’ data, and initialize Pusher:
var users = [];
var pusher = new Pusher({
appId: process.env.APP_ID,
key: process.env.APP_KEY,
secret: process.env.APP_SECRET,
cluster: process.env.APP_CLUSTER
});
Add a route for testing if the server is working. Don’t forget to access this route later on your browser once we run the server:
app.get("/", function(req, res) {
// for testing if the server is running
res.send("all green...");
});
Next, add the route for handling authentication requests (this is the endpoint that we’re accessing in the src/screens/TeamSelectionScreen.js
earlier). Here, we get the user’s index based on the username
in the request body. Only if the username doesn’t already exist do we process the request further. Once there are two users in the users
array, we trigger the opponent-found
event on both users. The event contains the pokemon_ids
and team_member_ids
for both users. That’s what we were making use of in the code for the battle screen earlier:
app.post("/pusher/auth", function(req, res) {
var username = req.body.username;
var pokemon_ids = req.body.pokemon_ids;
var team_member_ids = req.body.team_member_ids;
let user_index = users.findIndex(item => {
return item.username == username;
});
if (user_index === -1) {
users.push({
username: username,
pokemon_ids: pokemon_ids,
team_member_ids: team_member_ids
});
if (users.length == 2) {
var player_one_index = 0;
var player_one = users.splice(player_one_index, 1)[0];
var player_two_index = 0; // because there will only be one item left in the users array after the splice
var player_two = users.splice(player_two_index, 1)[0];
// trigger a message to each players. the message contains the IDs of the Pokemon of their opponent
pusher.trigger("private-user-" + player_one.username, "opponent-found", {
player_one: player_one,
player_two: player_two
});
setTimeout(() => {
pusher.trigger(
"private-user-" + player_two.username,
"opponent-found",
{
player_one: player_one,
player_two: player_two
}
);
}, 3000);
}
// authenticate the user
var socketId = req.body.socket_id;
var channel = req.body.channel_name;
var auth = pusher.authenticate(socketId, channel);
res.send(auth); // send a response back
} else {
res.status(400);
}
});
You might be wondering why there’s a three-second delay for triggering the opponent-found
event for the second user. This is because they joined last. This delay ensures that they’re already subscribed to all the relevant channels before the event is fired.
Next, make the server listen to the port
included in the .env
file:
var port = process.env.PORT || 5000;
app.listen(port);
Don’t forget to create the .env
file and put your Pusher credentials:
APP_ID=YOUR_PUSHER_APP_ID
APP_KEY=YOUR_PUSHER_APP_KEY
APP_SECRET=YOUR_PUSHER_APP_SECRET
APP_CLUSTER=YOUR_PUSHER_APP_CLUSTER
PORT=3000
Running the app
Once that’s done, we’re now ready to run the server and the app. Go back to the root directory of the app, start it, and run the server:
cd ..
expo start
node server/server.js
Next, navigate to the folder where you downloaded the ngrok executable file and run it:
./ngrok http 3000
Copy the resulting https URL to your src/screens/TeamSelectionScreen.js
file by replacing the placeholder value for the ngrok URL.
Conclusion
In this tutorial, we learned how to use Pusher Channels to implement the two-player Pokemon battle in React Native. We’ve specifically used it to pair players, and sync the attacks made by their Pokemon to their opponents Pokemon.
Stay tuned for the last part where we turn this app into a proper game by adding animations and sounds.
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 two-player
branch.
29 November 2018
by Wern Ancheta