How I got started in game development
I've been passionate about making games since I was a kid. My first experience with game development came through the Age of Mythology scenario editor, where I learned the basics of triggers, loops, and conditional statements. This introduced me to the fundamentals of programming and game development.
After experimenting with game modifications during my childhood, I created my first original game in 2014—a text-based RPG called Dragonslayer, using Python 2. Since then, I've worked on a variety of games and had the opportunity to participate in two community game jams.
Starting this project.
The concept of MMOs has always fascinated me. The freedom to choose your own experience in a world populated by thousands of players excites me. I love the social aspect, and the idea that, with an internet connection, you can always pick up right where you left off, with others to keep you company.
Up until now I had been working with databases and CRUD applications, and I'd also created single-player games. I knew I wanted to combine these experiences into a larger project.
1. Initial Stages
I began working in Unity, setting up a basic third-person character model and controller.
After that, I set the camera to an isometric point of view and added a few objects to the world.
To keep the player’s experience smooth, I created an invisible 3D point that followed and linearly interpolated its position based on the player’s distance. This allowed for free movement without affecting the point of view when near the center of the screen.
The goal here was to simulate how the game would look before adding any real content. I wanted to explore everything and get a feel for the project. I’ve always liked the pixel/low poly art style (think Runescape or Aberoth), so I planned to recreate that look. However, drawing out sprites seemed like too much work, so I initially experimented with scaling the resolution. Unfortunately, there were some drawbacks—namely, the interface would also scale down, making the menu unreadable.
Eventually, I found a solution. I would take the image from the character’s camera after it was drawn and "paste" it onto a 3D plane mesh, with the main camera pointed at it. This allowed me to scale the image down before drawing the interface, keeping the UI at a high resolution.
It was much better, but there was one major issue: there was no depth. The raycasting used to interpret the mouse position relative to the world didn’t work when the image was displayed on a plane mesh.
This took me forever to troubleshoot. Once I figured out the issue, I decided to abandon this approach.
2. Solved by Shader
After watching a few basic tutorials, I managed to put together a shader that solved the raycasting issue.
Although this worked, I wasn’t entirely happy with the way it looked. Since Unity’s render pipeline provided more flexibility, I decided to experiment further. I added a toon shader effect to simulate edge detection.
It looked nice, but a bit too neon for my taste, so I adjusted the colors.
At this point, I was much happier with the results. However, I began feeling a bit uneasy with all the policy changes around Unity, which led me to explore other options.
3. Switching to Godot
My friend approached me a couple months after I finished the initial prototype with a project of his own and asked if I’d like to collaborate. He had already set up a basic first-person controller, so I added some shaders to his project. Like before, we were able to scale the resolution without affecting the user interface.
The graphics still felt a bit bland, so I spent some time adding an internal calendar, day/night cycle with color blending, and implemented edge detection.
4. First Obstacle in Godot
Things were starting to feel much better, but I encountered an issue when implementing items in the overworld. Specifically, any 3D model with an image texture wouldn’t render properly after a certain distance from the camera. The items would be invisible but still "picked up" and populate the inventory.
To get around this, I tried replacing the image-textured 3D models with spinning plane meshes, textured in Blender. While this worked somewhat, it still didn’t feel quite right.
After speaking with others, the consensus was that this issue stemmed from an underlying problem with Godot’s renderer. Since I was using Godot 4.0 at the time (now on 4.3+), it was likely patched later, but I had to find another solution.
I ultimately decided to go back to using the 3D model approach. While more resource-intensive, it looked much nicer. I believe it will work great as long as the models remain low-poly and proper garbage collection is implemented to prevent item stagnation.
5. Adding Animations
While my friend wrapped up the inventory system and equipment handler, I focused on NPC animations. I added a rabbit model with functions to manipulate its animations and behaviors, to be triggered by the server.
I created three base animations and used time scales and animation blending to create smooth transitions, allowing more complex movements from simpler animations.
For testing, I set the rabbit to change its animation based on the distance from the player. The animations switched smoothly, showing that the linear interpolation on the transitions was working correctly.
6. Particles, Weapons, Destructible Terrain
By this point, both my friend and I felt more confident about the current state of the inventory/weapon system.
We also added a staff that could cast spells, and I enhanced the projectiles with particle effects. For example, arrows emit a trail-like particle, while fireballs emit flames, smoke, and an explosion of particles on impact.
Add in some mana, potions, and destructible houses and you can start to see it all come together.
7. Implementing the Server
Ideally, multiplayer games are designed with the intention of being multiplayer from the beginning. Since this was a prototype for educational purposes, we took the harder route.
To connect users to the server, my friend created a simple UI, and I added functions to capture the server info from the input box and establish the connection.
I set up a basic WebSocket server in Node.js and implemented essential functions like connecting the user, handling disconnections, and maintaining a heartbeat to check for dropped connections.
Once the handshake was established, I focused on persistence. When a player disconnects, the server saves their properties to the database. When they reconnect, their unique hardware ID is used to retrieve their data and update it.
I created server modules and stored data in a simple SQLite database, with constant items and spells tables.
The challenge was that database calls were synchronous, meaning the code would attempt to access values before the query had finished. To solve this, I implemented promises to ensure the server would wait for the response before continuing.
8. Security
For multiplayer games, server authority is crucial. Without it, players can manipulate data, such as movement speed. For example, a client could send a packet saying {'player':'User1', 'speed': 10}
, but what if the actual maximum speed should be 7? Server authority ensures the data processed by the server is valid.
Here’s how the server handles player positions.
We pass three vectors to represent the user and their position: Direction (where they are looking), Rotation (the user's orientation in radians), and Position (where they are relative to the center of the world). Knowing these values, along with the user's keyboard inputs, allows the server to simulate the movement of the players and send the data to all other players, linking those movement actions to a unique player ID which is displayed on each connected user's screen.
We can see after implementing that it's working, as the players are drawing the correct position, orientation, and direction of the player object.
There was a slight issue with the model's orientation in this clip— we were rotating the parent object instead of just the "head." Additionally, old player models weren't being discarded, creating a memory leak.
A few minor changes to the code later, and it's working great. We’re now sending inputs to the server with a direction, rotation, and position. Additionally, we're checking to make sure the distance the player has traveled since the last packet isn't too far. If it is, the server reverts to the original position to prevent speed hacks.
In that setup, I was sending roughly eight transform packets per second. Ideally, you would implement linear interpolation to "guesstimate" the player's direction and movements, allowing for less traffic over the channel and a smoother, faster experience.
9. Next Steps and Major Problems
While this all sounds great, there is an unforgivable flaw in this design that will ultimately require me to rebuild everything from scratch. When a player tells the server their new position, and the server checks the distance traveled to ensure they aren’t moving too fast, there’s no system in place to check for collisions.
Here is a rough diagram I’ve made to demonstrate:
As shown, when the player moves through the world, the server only checks for the distance traveled but doesn’t validate whether the player has collided with something like a wall or obstacle. The current system assumes that any move within the distance limit is valid, but this could allow players to bypass obstacles.
To fix this, I need to simulate all of the physics on the server and ensure that the server maintains authority over what collisions occur vs. what is simulated. The best solution seems to be adding server-side code into the game and designating one 'client' as the host. This host will simulate everything just like the engine would, but it can run in a graphic-less state to preserve resources.
Conclusion
This is as far as I’ve gotten in the year or so I’ve been working on this project. It’s my first-ever non-LAN multiplayer project, and I’m sure I will make more mistakes along the way. But this experience has taught me so much, and I feel much more confident and prepared to tackle round two of this project.
Top comments (0)