
Matrix: Building a real-time RPG game with Convex

Matrix : Building a real-time RPG game with Convex is a guest Stack Post from the community by Convex Champion, Jashwanth Peddisetty.
This article is a log on how I built a fun RPG game to learn languages by chatting with nearby players, using Convex for real-time updates and AI for translations along with discussion on some of the cool tools that helped me in developing the application quickly
Problem I was trying to solve
This year, I moved to a new place where people speak a different language than my mother tongue. Although English can be a common language for communication, I really wanted to learn the local language as a fun experiment.
I found language learning apps too boring and soon realized that the only effective way to learn a new language is by speaking it frequently. Through conversations, we gradually build vocabulary and eventually become fluent. However, this process can be particularly challenging for introverts like me. That’s when I got the idea to create a real-time game where everyone can join a map augmenting real-world situations.
Game idea
Similar to Minecraft, it’s a real-time RPG game where you can talk to people you come close to on the map. This would help users talk or chat with new people or AI NPC bots in the map and, in the process, learn the language.
I wanted a backend infrastructure to store the real-time locations of players and update their coordinates on all devices with the game open. Additionally, whenever two players came close, the chat interface would open quickly to enable communication between them.
Features I wanted:
- A real-time way to store the coordinates of players
- No need to handle all synchronization issues manually
- A free tier without requiring a credit card to start
- Good integration with Next.js
convex for reactive DB
Convex had all the features I wanted for the application, so I proceeded with Convex as the backend and Next.js for the frontend. The templates in Convex made it very easy to get started. I wanted to use Next.js, Convex, and Clerk, and the Informal template was a perfect starting point for me.
Building the Frontend for the game
With a basic UI idea and user flow in mind, I started with v0, using prompts to generate the frontend for the game scene and map selection scene. To my surprise, the outputs from v0 were so good that I had to write very little code for the frontend myself. For those who don’t know v0 is a frontend code generator website that works really well with shadcn , tailwindcss and Nextjs which are the exact techstack that we are currently using.
Game scene of real-time movements of players
Building the Backend Logic
ChatGPT + Convex = 🔥. I observed that by providing a sample mutation and query, ChatGPT can generate the entire database plan, schema.ts
, and all the mutations and queries required for the backend logic. There are three main tables that i have used
- Map Table
- Chat Table
- Player Table
Schemas Used
Map Table
It’s just a table configuration to store the dimensions and name, but in the future, it can store map tile images, nature, weather, and much more.
I added an index for map_name
so that anyone searching for a particular map name can quickly retrieve the map details.
1 maps: defineTable({
2 dimensions: v.object({
3 height: v.float64(),
4 width: v.float64(),
5 }),
6 map_name: v.string(),
7 }).index("map_name", ["map_name"]),
8
Player Table Any player should be present in only one map at a time. To fix this, instead of storing players in the map table, I have added x_coordinate, y_coordinate, and map_id to the player table itself. To make the search faster, I have added an index to the player's email ID
1players: defineTable({
2 img_url: v.string(),
3 player_mail: v.string(),
4 present_map_id: v.optional(v.id("maps")),
5 x_coordinate: v.float64(),
6 y_coordinate: v.float64(),
7 })
8 .index("player_mail", ["player_mail"])
9 .index("present_map_id", ["present_map_id"]),
10});
11
Chat Table
The chat table stored the details of each message and its corresponding sender-receiver pair. Encryption should have been added, but for the current implementation, I have left it as it is.
1 chat: defineTable({
2 messages: v.array(
3 v.object({
4 message: v.string(),
5 receiver: v.id("players"),
6 sender: v.id("players"),
7 timestamp: v.float64(),
8 })
9 ),
10 receiver: v.id("players"),
11 sender: v.id("players"),
12 }).index("sender_receiver", ["sender", "receiver"]),
13
One thing I really like about Convex is that it offers amazing analytics and the ability to manage both production and development environments as easily as changing environment variables.
After the schema, the next most important aspects are the mutations and queries. I personally love the syntax of their mutations and queries. Unlike ORMs, they have a simple, code-like syntax that makes the developer experience truly great.
Whenever two players come close to each other, we enable the chat option between them. If this were any other traditional database, it would have taken a lot of effort to create this real-time functionality. But with Convex, it’s as simple as writing a better query.
Fetching Realtime player proximity
The following code is the query I used to present the chat options for players who are close to the current player:
1 export const nearPlayer = query({
2 args: {
3 player_mail: v.string(),
4 map_name: v.string(),
5 },
6 handler: async ({ db }, { player_mail, map_name }) => {
7 const map = await db.query("maps")
8 .withIndex("map_name", q => q.eq("map_name", map_name))
9 .first();
10 if (!map) {
11 throw new Error("Map not found");
12 }
13
14 const player = await db.query("players")
15 .withIndex("player_mail", q => q.eq("player_mail", player_mail))
16 .first();
17 if (!player) {
18 throw new Error("Player not found");
19 }
20
21 const playersOnMap = await db.query("players")
22 .withIndex("present_map_id", (q) => q.eq("present_map_id", map._id))
23 .collect();
24
25 const { x_coordinate, y_coordinate } = player;
26
27 const nearbyPositions = [
28 { dx: 0, dy: 0 },
29 { dx: 1, dy: 0 },
30 { dx: -1, dy: 0 },
31 { dx: 0, dy: 1 },
32 { dx: 0, dy: -1 },
33 ];
34
35 const nearbyPlayers = playersOnMap.filter(otherPlayer => {
36 if (otherPlayer._id === player._id) return false;
37
38 return nearbyPositions.some(pos => {
39 const targetX = x_coordinate + pos.dx;
40 const targetY = y_coordinate + pos.dy;
41 return otherPlayer.x_coordinate === targetX && otherPlayer.y_coordinate === targetY;
42 });
43 });
44
45 return nearbyPlayers.map(player => ({
46 _id: player._id,
47 player_mail: player.player_mail,
48 img_url: player.img_url
49 }));
50 },
51});
52
Writing similar database interactions with real-time features in other databases could have been a daunting task. Thankfully, Convex is here to save us with its intuitive syntax. For any player, I searched for all the players who are on the same map and are just one block away from the player.
Bringing Magic of AI into the application
Now that the basic implementation of the map, movements, and chat is done, we have added AI functionality to convert the message text into the language specified by the user. OpenAI perfectly handles the translation and provides accurate results.
Do you wanna build a RPG game? use this template
Following is the github repo of the Game and this has the entire boiler plate code to maintain maps, skins , movements and authentication. its easy to install it as well, looking forward for more fun games to be built with this template.
Convex is the sync platform with everything you need to build your full-stack project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.