How to Make a Turn-Based RPG Game in Phaser – Part 2

In the last tutorial, we created the BattleState for our turn-based RPG. Now, we are going to create a WorldState were the player can explore and eventually find enemies. In addition, we will improve our battle system to consider units speed. The following topics will be covered in this tutorial:

Creating a WorldState which the player can navigate

Creating enemy spawners that will initiate the BattleState

Improve the battle system to consider units speed

To read this tutorial, it is important that you are familiar with the following concepts:

Source code files

Creating the WorldState

We are going to create a WorldState which will read a Tiled map (in JSON format) and allow the player to navigate through it. The figure below shows the map I created. Since the focus of this tutorial is not on creating Tiled maps, I’m not going into the details of it, and you can create your own or use the one provided in the source code. However, two things are important about the map creating, due to the way we are going to create WorldState:

Any collidable tile layer must have a collision property set to true.

All game prefabs must be defined in the objects layer and each object must contain in its properties at least: name, type, group and texture. Any additional properties must also be defined.

The figures below show the properties of a collision layer and the player object, to illustrate those two conditions.

In addition to the Tiled map, WorldState will read another JSON file, such as the one below. Notice that the JSON file must specify the assets, groups and map information.

The code below shows the WorldState. The “init” method initializes the physics engine and creates the map form the JSON file. It also creates a “party_data” object which contains the stats of all player units. Notice that this object can be passed as a parameter, which will be done after each battle.

The “create” method must initialize the map layers and game groups and prefabs. The layers were all available in the Tiled map and were already read in the “init” method, so we just have to iterate through them creating each one. However, we must detect the collision layers (by means of the added property) and set all its tiles as collidable.

The groups are easily created by iterating through the ones defined in the level JSON file. However, to create the prefabs we must iterate through each object in the objects layer and instantiate the correct prefab. The prefab instantiation is done in the “create_object” method, which also adjust the prefab position due to the different coordinates used by Tiled. To properly instantiate each prefab, we define a “prefab_classes” property in the constructor that maps each prefab type to its constructor, as we did in the BattleState. As in BattleState, this is possible because all prefabs have the same constructor.

The last piece of code in the “create” method resets the player to a previously saved position. This must be done because the WorldState can be called after a BattleState, so we must keep saved the player previous position. If the battle is lost, we reset the game using the “reset_position” parameter in the “init” method.

Adding the Player prefab

Now we are going to add the player prefab, which can navigate through the world. The code below shows the Player prefab. In the constructor it must initialize the walking speed, animations, and physics body. Then, in the “update” method, it is controlled by the keyboard arrow keys (obtained from the “cursors” object). We move the player to a given direction if its respective arrow key is pressed and if the player is not already moving to the opposite direction. Also, when the player starts moving to a given direction it plays the corresponding animation. When the player stops, the animation stops and reset its frame to the correct stopped frame, according to its facing direction.

You can already try moving the player in the WorldState. Check if the collisions are working properly. Notice that we check for collisions with all collision layers in the update method.

Adding the EnemySpawner prefab

The EnemySpawner will be an immovable prefab that overlaps with the player. When such overlap occur, it will check for possible enemy encounters according to their probabilities. We will start by defining the enemy encounters in the JSON level file, as shown below. Each enemy encounter has a probability and the enemy data.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

"enemy_encounters":[

{"probability":1,

"enemy_data":{

"bat1":{

"type":"enemy_unit",

"position":{"x":100,"y":90},

"properties":{

"texture":"bat_spritesheet",

"group":"enemy_units",

"stats":{

"attack":10,

"defense":1,

"health":30

}

}

},

"bat2":{

"type":"enemy_unit",

"position":{"x":100,"y":170},

"properties":{

"texture":"bat_spritesheet",

"group":"enemy_units",

"stats":{

"attack":10,

"defense":1,

"health":30

}

}

}

}

}

]

Now, we can create the EnemySpawner to check for one of the possible encounters. Its code is shown below. In the “update” method we check for overlaps with the player and call the “check_for_spawn” method when an overlap occure. Notice that, to call this method only once for each overlap we use the “overlapping” variable, and check for spawns only when it was false.

The “check_for_spawn” method generates a random number using Phaser random data generator and compares it with the enemy encounters probabilities, choosing the first one whose probability is higher than the generated number. Notice that, for this to work the encounters must be sorted in ascending order of probability, prioritizing less likely encounters. When an encounter occurs, it calls BattleState with the correct enemy data and player party data.

Updating BattleState

Finally, we must update our BattleState to work with our last modifications. Instead of reading the enemy and player units from a JSON file, they will be passed as parameters in the “init” method. Then, we just have to iterate through them in the “create” method and create all their prefabs, using the same “create_prefab” method. Notice that the enemy units were stored in the enemy encounters data from the WorldState, while the player units were stored in the “party_data” variable from WorldState (shown before).

Now, we must properly go back to WorldState when the battle is finished. In the “next_turn” method, before making the next unit act, we check if there are remaining enemy and player units. If there are no remaining enemy units, we call an “end_battle” method and, if there are no remaining player units we call a “game_over” method.

The “end_battle” method will switch back to WorldState updating the “party_data” to reflect this battle. So, we must iterate through all player units saving their stats in the “party_data” variable. On the other hand, the “game_over” method will switch back to WorldState without sending any “party_data”, which will reset it. Also, we must tell the WorldState to restart the player position, instead of keeping the last one as showed in the WorldState code. The code below shows the modifications in the BattleState.

Considering units speed for the turns

In this tutorial, each unit will have a speed stat, which will be used to calculate the next turn that unit will act. The code below shows the modifications in the Unit prefab, and how it calculates the next act turn based on the current turn and its speed. Notice that I used an arbitrary rule for calculating the next turn, and you can use the one you finds best.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

varRPG=RPG||{};

RPG.Unit=function(game_state,name,position,properties){

"use strict";

RPG.Prefab.call(this,game_state,name,position,properties);

this.anchor.setTo(0.5);

this.stats=Object.create(properties.stats);

this.attacked_animation=this.game_state.game.add.tween(this);

this.attacked_animation.to({tint:0xFF0000},200);

this.attacked_animation.onComplete.add(this.restore_tint,this);

this.act_turn=0;

};

RPG.Unit.prototype=Object.create(RPG.Prefab.prototype);

RPG.Unit.prototype.constructor=RPG.Unit;

RPG.Unit.prototype.calculate_act_turn=function(current_turn){

"use strict";

// calculate the act turn based on the unit speed

this.act_turn=current_turn+Math.ceil(100/this.stats.speed);

}

Now, we will change the BattleState to store the units in a priority queue, instead of an array. A priority queue is a data structure where all elements are always sorted, given a sorting criteria (if you’re not familiar with priority queues, check this wikipedia link). In our case, the sorting criteria will be the unit next acting turn, which means the first unit from the queue is the one that will act earlier. Since the priority queue is a well known data structure, we are going to use the implementation provided by Adam Hooper instead of creating our own.

The code below shows the modifications in the BattleState to use the priority queue. First, in the end of the “create” method we initialize “units” as a priority queue which compares the units act turn, and add all units to the queue, calculating their first acting turns. Then, in the “next_turn” method, we must update the current unit act turn before adding it to the “units” queue again, so it will be put in the correct position.

Now, you can already try setting some speed values and see if everything is working correctly. Below is the final enemy encounters and party data I used.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

"enemy_encounters":[

{"probability":0.3,

"enemy_data":{

"lizard1":{

"type":"enemy_unit",

"position":{"x":100,"y":100},

"properties":{

"texture":"lizard_spritesheet",

"group":"enemy_units",

"stats":{

"attack":30,

"defense":10,

"health":50,

"speed":15

}

}

}

}

},

{"probability":0.5,

"enemy_data":{

"bat1":{

"type":"enemy_unit",

"position":{"x":100,"y":90},

"properties":{

"texture":"bat_spritesheet",

"group":"enemy_units",

"stats":{

"attack":10,

"defense":1,

"health":30,

"speed":20

}

}

},

"bat2":{

"type":"enemy_unit",

"position":{"x":100,"y":170},

"properties":{

"texture":"bat_spritesheet",

"group":"enemy_units",

"stats":{

"attack":10,

"defense":1,

"health":30,

"speed":20

}

}

}

}

},

{"probability":1.0,

"enemy_data":{

"scorpion1":{

"type":"enemy_unit",

"position":{"x":100,"y":50},

"properties":{

"texture":"scorpion_spritesheet",

"group":"enemy_units",

"stats":{

"attack":15,

"defense":1,

"health":20,

"speed":10

}

}

},

"scorpion2":{

"type":"enemy_unit",

"position":{"x":100,"y":100},

"properties":{

"texture":"scorpion_spritesheet",

"group":"enemy_units",

"stats":{

"attack":15,

"defense":1,

"health":20,

"speed":10

}

}

},

"scorpion3":{

"type":"enemy_unit",

"position":{"x":100,"y":150},

"properties":{

"texture":"scorpion_spritesheet",

"group":"enemy_units",

"stats":{

"attack":15,

"defense":1,

"health":20,

"speed":10

}

}

}

}

}

]

this.party_data=extra_parameters.party_data||{

"fighter":{

"type":"player_unit",

"position":{"x":250,"y":50},

"properties":{

"texture":"male_fighter_spritesheet",

"group":"player_units",

"frame":10,

"stats":{

"attack":15,

"defense":5,

"health":100,

"speed":15

}

}

},

"mage":{

"type":"player_unit",

"position":{"x":250,"y":100},

"properties":{

"texture":"female_mage_spritesheet",

"group":"player_units",

"frame":10,

"stats":{

"attack":20,

"defense":2,

"health":100,

"speed":10

}

}

},

"ranger":{

"type":"player_unit",

"position":{"x":250,"y":150},

"properties":{

"texture":"female_ranger_spritesheet",

"group":"player_units",

"frame":10,

"stats":{

"attack":10,

"defense":3,

"health":100,

"speed":20

}

}

}

};

And now we finished this tutorial. In the next one we are going to add different actions during the battle, such as using magic and items. In addition, the player units will receive experience after each battle, being able to pass levels.

Published by

Renan Oliveira

Renan is a computer science master student and game enthusiast. His interest in game development started a few years ago with a 2D game engine course, which resulted in a small 2D engine and game. He started working with Javascript and Phaser with the Zenva Game Development Course. Currently, he is working in his own game.
View all posts by Renan Oliveira