Development progress as the final technical presentation approaches
Since movement is an essential part of a game, we spent a lot of time working on its features and consequently encountered several issues. At the beginning of development, the character moved too quickly and jumped too far. By reducing the character's velocity, the gameplay felt more natural. Additionally, a “sliding” sensation occurred when running fast, which we removed to make the player feel like they were running on dry wood, not ice. We also encountered abnormal behavior when jumping while sticking to a wall. The player would get stuck. The issue came from a loop that forced the player downward whenever their collision box touched a wall or ceiling. Removing this loop introduced another, less problematic bug where the player would stay stuck to the ceiling during their jump. We later fixed this by checking whether the player was touching the ceiling during the jump. The ladder implementation was revised multiple times to fix bugs, such as missing collisions in certain areas. Additionally, the character was supposed to cling to the wall with a ladder in order to climb it, but a collision issue allowed the player to climb without actually touching the ladder. We resolved this by creating a specific tilemap for ladders. A check was added to ensure that the player could only stand up if there was enough space above them. This check detects if a ceiling prevents standing up, which ensures more realistic behavior and avoids collision problems. Initially, the crouch system did not account for the environment above the player. This meant the character could occasionally stand up even when there was an obstacle (like a low ceiling) directly overhead, leading to collision issues and the player potentially getting stuck in the level geometry. Synchronizing animations, such as jump and its associated animation, was also challenging, as the jump consists of different states (e.g., takeoff, in-air, landing, etc.). On top of that, attack and dash animations could overlap with the jump, further complicating synchronization. Jumping also had other issues—one bug allowed the player to convert their jump velocity into an upward propulsion. This was due to Unity's automatic generation of hitboxes, which didn’t always precisely match the shape of the level’s blocks. Instead of using the automatically generated hitboxes, we manually defined one. This allowed better control over the character’s interactions with the environment and prevented abnormal behaviors, like being launched into the air. Similarly, another bug occurred when jumping in a certain way into the ceiling, breaking immersion by causing collision issues and letting the player clip through it. The use of dash was also a source of bugs—players could sometimes dash through walls, which was a major problem. This happened mainly because the high speed of the dash allowed the player to bypass Unity’s automatic collision detection.
During the creation of the assets, many challenges emerged. First, the animations took a long time to produce because the default mode in Pixel Studio didn’t allow for efficient animation workflows. However, we discovered a more advanced mode that optimized the animation process and significantly reduced the time required. That said, when we later decided to slice all the animations, it meant reworking every single one of them—an effort that was quite time-consuming. Additionally, in pursuit of realism, several animations had to be modified multiple times to better match the game's artistic direction. Second, the tiles for zone 1 were too dark. Without assembling the created assets in the game environment, we didn’t initially notice how overall gloomy the result was. The colors lacked variety and skewed toward a dull, bark-brown tone that not only discouraged gameplay but also strained the eyes over time. Furthermore, it was particularly difficult to find an effective way to depict termite tunnels, and the final result wasn’t obvious during development. Third, by default, Unity smooths out images to blur the pixel edges. However, in our case, having large, visible pixels was an intentional stylistic choice. We had to manually change Unity’s rendering settings to ensure that our pixel art remained crisp and properly displayed. Finally, there was an issue with seams appearing between certain tiles in the level, which became noticeable as the character moved. To fix this, we used a Sprite Atlas to group our assets together. This not only improved performance but also resolved certain positional rendering issues at runtime. However, the Sprite Atlas wasn’t fully functional when we created the bark textures, and even in the latest version of the game, we occasionally encounter unpredictable Sprite Atlas bugs that appear and disappear without clear cause.
As explained above, the design of the website posed no major challenges. The only more difficult part was mastering CSS to find the right color schemes, dimensions and shapes for the different elements of the site.
When integrating multiplayer into our game, we quickly noticed that the position of the client who was not the host was reset to zero every time they tried to move. It turned out that the issue stemmed from how multiplayer works in *Mirror*. There are two main ways to structure multiplayer in a game. In the first approach, the client has authority: it moves its own character, and the server simply relays that information to the other players. In the second approach, the server must validate every action, including player movements and any data exchanged between players. In our initial implementation, we designed the movement script to run locally under the assumption that each client would control their own character freely. In reality, only the host player's movements were accepted—any movement from the other clients was overridden (and effectively canceled) by the server. To address this, we had to rethink how player movements were handled and how they interacted with *Mirror*. The simplest solution was to assign authority over each player's objects to the client controlling them. We also faced difficulties transitioning from a locally functioning game to one with synchronized clients. First, we switched tools to better synchronize Unity clients. We had to manage both a solo mode, which runs on a local server, and a multiplayer mode structured around a remote server. We chose to use a *state machine* to manage the game state, load levels, trigger procedural generation with the same seed for all clients, and teleport players. Learning how to use the various components of Mirror to send such information was one of the main challenges. While the library appears easy to use at first, synchronizing more complex states requires a deep understanding of all the tools it offers. Additionally, the “host client” concept—where a player acts as both client and server—added more complexity. Our organizational decision to designate a single person in charge of multiplayer also caused issues. Game features were often designed and programmed without considering how they would be implemented in multiplayer, making later integration more difficult and leading to frequent synchronization problems. For example, after developing the player's movement system, we had to rework the script to make it multiplayer-compatible. The same applied to the enemies and, more broadly, to the combat system. The most difficult element to synchronize—and one that can still malfunction—is enemy synchronization. We had to ensure that all clients saw the same enemy health, targeted player, movement, animations, and so on. Movement synchronization was especially challenging because the pathfinding algorithm was running locally for each client rather than on the server, which complicated coordination. Unfortunately, due to time constraints and organizational challenges, we weren’t able to fully redesign our multiplayer architecture to be more server-centered. While this would have made development more demanding, it would also have allowed for a more stable and functional multiplayer system.
Creating the rooms took longer than expected, as each one required careful planning beforehand—a process detailed in the progress description for this part of the project. Additionally, with an average size of 80 by 40 *tiles*, and each *tile* being 16 by 16 pixels, a large number of tiles had to be thoughtfully placed to design a level that was both original and optimized. Finally, as the game evolved, along with our thinking and the development of our skills, some of the earlier rooms became obsolete, as they no longer met the new standards we had set for ourselves. This led to their removal from the project. As for the rooms in level 2, several were created, but the level itself couldn’t be implemented due to a lack of time.
One of the first PoC challenges for procedural generation was to ensure the validity of the levels produced. One of the first ways of doing this was to show all the information for each part in the graph and check by hand. This method works at first, but becomes limiting as soon as we try to make a level of more than three rooms. To generate a graphical representation, we first used a graphics library to make a pixel-by-pixel image. By default, all image readers smooth pixels, which has an impact on rendering visibility. After some time spent trying to get around this problem, we turned to the much more flexible SVG format.