In previous articles, we’ve discussed data model that could run card games, board games and even MMO games. We've also developed a simple tic-tac-toe game using Spring Boot and AngularJS. In this article we’ll move to the next level and develop a model that can store results of an “action” game.
Let’s get started!
Before We Start Modeling
Before we get into designing the database, we need to know some background on the type of games we’re dealing with and what they will require.
What Are Action Games?
In general, an action game mainly focuses on actions like moving, flying, jumping, shooting, fighting, collecting items, etc. This is a comprehensive genre that includes some very popular titles like Tomb Raider, The Legend of Zelda, Assassin’s Creed, and Doom. I would add games like Angry Birds, Pac-Man, Bomberman, and Worms to this category. They may be less visually attractive, but they are fun and addictive. They also combine action and strategy.
So, to sum up, we can say that action games require fast reactions, and there’s often less time for planning than players would like! Sure, it can be argued that true action games differ from other action-oriented games in the amount of adventure, shooting, and strategy they employ. But across the genre the goal is to keep the player intensely focused on the game.
Action games share several common characteristics, including:
- Games have different maps or levels.
- Each level has various obstacles.
- Each new level has more challenging opponents than previous levels.
- Each new level has more challenging opponents than previous levels.
- Action! Players may find themselves wearing out bits of their keyboard :)
Our database won’t store each keystroke or everything players do during the game. We will keep info about levels and related maps, objects, opponents, and opponent’s skills. We’ll also store current data about players’ character(s), skills, and progress.
Where Can We Use This Database Model?
This data model can store the results of a web-based action game. Users can create a profile, choose character(s), and start the game. In the database, we’ll keep snapshots of the game at set points, e.g. after each level is completed. Some game parameters are stored in the database, while real actions will take place in the interface.
Who Is the Protagonist and Who Is the Villain in Our Game?
Every game has a protagonist and a villain. In action games, each level usually has opponents with the same or similar characteristics, plus one boss villain. Think of simple 2D space shooting games. Your protagonist will probably be a spaceship, and you’ll shoot at asteroids and alien ships until you get to the boss on that level. In our database, it’s important that each villain (or set of villains) has their own skills and a different visual representation.
How Long Does the Game Last?
When the game has more than one level, we’ll move on to the next level after we beat the boss. Most games have a limited number of levels; when you complete the final level you’ve also completed the game. But maybe we don’t want to have a limited number of levels. In that case, we’ll need to generate another level after one is completed. Of course, that complicates things a little!
I’ll go with the assumption that our model should cover both these options.
The Data Model
Our model is divided model into four subject areas:
Character, Opponents and Skills
We’ll start with the
Game setup area because it contains the tables that are most important for game mechanics. We will then move to other areas in order of their importance.
The Game Setup Area
Game setup area, we’ll store everything relevant to each game instance: levels, maps, objects, and opponents. A game instance is basically a saved game. Maybe we’ve started playing the game with one character and got to Level 4. Then we saved our game and started again with another character. In that case, we would have two game instances. For each of these instances, we’ll store separate information about the current level, character skills, maps, etc.
Because we reference it in three other tables in this section, we’ll start with the
level table. It has only one attribute and that is
id. If we have a finite number of levels in the game, this table will contain a list of ordered numbers. So if we have 20 levels in a game, we’ll have numbers from 1 to 20 in this table.
If we want to have an indefinite number of levels, we’ll have to generate a new level after the previous one is completed. For each new level, we’ll add a new number to this table. (E.g. if we already have numbers from 1 to 20, after we complete 20th level, we’ll start 21st level and add "21" to this table.) The
id attribute is the primary key of the table; its
AUTO_INCREMENT property is set to
False. That seems safer here, but either options should work.
Each new instance of the game will be stored in the
game_instance table. This is the most important table in this section. We’ll store the
player_id that started each game instance, plus the actual start time and the current level. The
current_level_id attribute can be NULL, since a new game will not have a level yet.
We can expect that each level will have a related map. Some maps are always the same when the player starts that level. In that case, all objects placed on the map will be stored as fixed position objects in our database. The list of all these objects and their positions is basically our map template.
In some cases, we will generate a new map each time the level is started. Some objects and enemies will be placed on the same locations; others will be placed on random locations. Randomization is very important to keep players playing and replaying the game.
We’ll need to store both fixed and randomized maps. The
map table will relate maps with levels and game instances through foreign keys. It will also store the actual time when the map was generated and the score the player achieved for that map or level.
For each map, we’ll store a list of all objects and their positions. To do so, we’ll use the
map_object table. We’ll store references to the
map table (
map_id) and the
object table (
position attribute saves an object’s actual coordinates in text format. We could format object positions for a 2D map [e.g. "22:5"] or a 3D map [e.g. "22:5:10"]. Finally, the
random_position attribute defines if the object is placed randomly or not.
object table contains a list of all objects that will appear on a map. Each object will have a unique
name and an
object_image (if needed). For example, objects in a space shooting game include planets, asteroids, space stations, background images, etc.
If our game has a limited number of levels, we could define fixed positions for some objects and place others randomly. If we have an unlimited number of levels, we’ll need more randomization when we generate maps for new levels; even so, some objects could still be fixed. In both cases we would use data stored in the
fixed_level_object table. For each level, we’ll store every object that will appear in exactly the same
position each time we generate a new level map. These records will be copied to the
map_object table and will have the
random_position attribute set to
The remaining map objects will be placed randomly. To do so, we’ll need to develop a random map generator that will take input parameters (e.g. the current level), calculate the number of each object type for that level, place fixed-position objects first, and then calculate random positions according to set rules (e.g. distance from other objects). Random map generators can be extremely complex and they are outside the scope of this article.
It is important to note that if all objects on our map are fixed we’ll have redundant data in the
map_object tables. Each time somebody starts the same level, they’ll generate exactly the same map and store exactly the same data in these tables. Still, I would leave the model this way because we can expect at least some randomization during the map-generation process.
Before we move on, let me quickly explain the concept of opponent waves. Opponents generally come one after the other in “waves”. Sometimes they are in groups, which could be composed of different types of opponents. We need to be able to define the number and order of these groups.
This leads us up to the last table in this subject area, the
opponent_group table. This table defines the types and numbers of opponents on a level. Values are inserted only once for each level. If the game has a finite number of levels, we can expect that these values will already be filled. If the number of levels is not set, we’ll calculate the number of opponents and positions when we reach a level that has no defined parameters. For this, we’ll use the formula stored in the
Let’s take a quick look at the attributes in this table:
level_id– The ID attribute of the related level
opponent_id– The ID of the related opponent. The
opponent_idattribute pair is NOT the unique key for this table. This enables us to have the same types of opponents in many different waves – e.g. as you would in tower defense games.
opponent_no– The number of opponents in that wave
- “opponent_order” – This sets the wave order – which comes first, which second, etc.
Notice that we could have different opponents that have the same
opponent_ordernumber. In that case, the wave would be composed of mixed opponents – e.g. two different types of alien ships.
The Characters, Opponents and Skills Area
This area contains core formulas needed to run the game correctly. You’ll notice that three tables –
opponent_skill – contain formulas. Each of these formulas should keep the game interesting and balanced.
character table contains a list of characters that players can use. The only attribute here is “name”, which must contain a unique value.
character table, the
skill table only has a
name attribute that can only contain unique values. Common character skills include firepower, armor, shield, or energy.
Characters are defined by their skills and how these skills evolve through levels. In a space shooter game, some spaceships will have stronger armor; others will have more firepower. In the
character_skill table, the foreign key pair
skill_id forms the unique key of the table.
level_formula attribute is a text representation of the formula used to calculate new skill values. When a character reaches the next level, they increase their skill rating. We’ll calculate these new values using the information in
level_formula and update the
actual_skill”.”skill_value attribute with these values. (The “level” parameter used in this formula is
A list of all opponents in our game is stored in the
opponent table. Once more, the
name attribute is this table’s unique key. The
level_formula attribute is used to calculate the number of opponents on the level as well as their position in the waves. We’ll use this formula only when we don’t already have these values in the
opponent_group table. When we need to store these values for a new level, the formula result will be inserted into the
opponent_group table during level generation. The “level” parameter for this formula is the
opponent_skill table, we’ll store the skills assigned to different types of opponents. The foreign key pair
skill_id is the unique key of the table;
level_formula defines how this skill increases with each new level in the game.
The Actual Skills Area
This small but important area stores all characters and skill levels for a game instance.
In most games, a player will play just one character at a time. However, some games let players have multiple characters and choose among them. (In a space shooter game, this is when you own a fleet of spaceships and select one for the next mission.) We’ll store this character list in the
character_used table. A player could define a
character_name for his character. We’ll also need to store references to the
game_instance table and the
character table. The current character level is stored in the
A character’s skills change as it progresses through the game. We’ll store the current skill values in the
actual_skill table. A pair of foreign keys –
skill_id – form the unique key of the table. The character’s current skill value is stored in the
The Players Area
Players subject area, we’ll store all player-related details, such as registration information and login history.
First, all registered players are listed in the
player table. For each player, we’ll store a username, password, and screen name in the
nickname attributes respectively. The
nickname attributes are all unique.
New users will need to provide an email address during registration. In order to complete the registration process, a confirmation code will be generated and sent to them. After they reply, they can start playing. We will update the
confirmation_date attribute when the user verifies their email address.
Each time a user logs in, we’ll add a new record in the
login_history table. We’ll store the exact login time and some other details (geographic location, IP address, device and browser used). When the player closes the game, we’ll update the
Designing a database to store information for different types of action games is a very demanding task. With this model, I tried to cover the basics: users, characters, opponents, objects, levels, and maps. Using formulas to calculate important values like character skills and the number of opponents should keep this model flexible enough to cover games with an unlimited number of levels.
There are many upgrades we could make to this model. Please feel free to share your ideas and suggest some possible improvements in the comments section.