OH NO My Car Broke Down

A PS1 style game made with Unity for a university game development course

NO_IMAGE

Main menu of the game Oh No My Car Broke Down

The Game

Last semester I enrolled in a beginner course about game development at my university. I have always been fascinated by game development and I finally saw my chance at creating one, that is more than a demo. Together with a fellow exchange student we worked hard on OH NO My Car Broke Down for three months. I was tasked with audio, video effects, rendering and modeling. My team member mostly concentrated on programming features like movement, interaction with the environment and more.

As we were planning a game we agreed on developing a "horror game" that sort of looks like a PS1 game like Silent Hill or Resident Evil. It was clear that this would not be an easy task as Unity's rendering backend is geared towards modern rendering aesthetics. But I knew, that I was up to the task as I had worked with shaders in the past and I could use my knowledge about 3D rendering I gathered while studying for my bachelor's degree.

In the end we managed to create a workable game, that although not being scary, looked and felt awesome to play. As the semester drew to a close and the submission deadline loomed on the horizon, quite a few bugs needed to be addressed. I remember working on a critical bug regarding save states of picked up items while riding a train to Hannover on the last day before submission. We managed to pull though and were able to present the finished product to our fellow students. I was quite surprised to see more than 100 students attended the game presentations, of all games created by course participants. The presentation went smoothly and there were very few questions asked which might have to do with the overwhelming visuals of our game. Our game was one of very few 3D submissions and stood out from the crowd regarding visual quality. I might be biased, but I would say that our game was the best looking one by far.

Overall I am quite pleased with the final product and I will come back to it in the near future, to fix some small issues and hacks, we implemented in the last few days of development.

3D Graphics

I think it is only fair to dedicate a chapter to 3D graphics as this was the area in which our game stood out the most. Working with Unity's rendering pipeline is quite a challenge. The good thing I have to say is, that Unity makes it quite easy for new developers to get something visually pleasing to the screen with very little time investment. Well. That is all the good things I have to report. These were the most annoying aspects of unity when trying to create PS1 like visuals:

  • Data related to light sources (position, direction, color, type) are well hidden in the shader code and most values are not documented, or documentation is very thin
  • Unity decides which light sources are visible and should be used in rendering, but frequently makes incorrect decisions, which leads to lights popping in and out of existence
  • Passing custom data buffers to shaders is way too cumbersome and not really worth it
  • Custom tessellation of quad surfaces is very broken for most graphics APIs and only really worked reliably with Vulkan and DirectX 11/12
  • Rendering a complete game (scene and text overlay) at a lower resolution is not easy, and render textures were quite buggy when using OpenGL or Metal
  • The API for implementing post-processing passes in Unity changes all the time and is not very consistent
  • Adding black bars to keep a 4:3 aspect ratio in Unity is a hassle and not worth it
  • Blitting of textures to the screen frequently resulted in graphical glitches when using Metal
  • Although Unity supports many graphics APIs through the same interface, the visual output may vary significantly and testing for all APIs is difficult
  • Unity technical staff does not really seem interested in supporting low res games like the one we developed

The list is not complete, and I probably already forgot some of the most annoying issues I encountered. In the end I had to create a custom post-process effect and custom shaders with custom lighting to trick the Unity rendering backend into producing PS1 like visuals. It would be great if only Unity had issues with this artistic style, but I recently tested Godot and per vertex lighting seems to be broken completely. I do not know what the problem is, but creating games that look like games from the PS1 and N64 era is surprisingly challenging in modern engines.

Lighting

The PS1 supported a per vertex lighting model very similar to Gouradu Shading. I recreated this version of lighting in our various shaders. Each material supports a texture, with a diffuse light coefficient and a specular light coefficient to compute a surface's brightness based on the Phong reflection model. Ambient lighting is then added on top and vertices further away from the camera are occluded by fog of a given color. Fog strength is based on light intensity, such that a light source "cuts through" fog.

Dynamic lighting such as moving light sources are supported, although the PS1 was not really support these. Silent Hill only had one moveable light source, the flashlight. In our game this is the same, to keep the illusion.

Shadows are baked into scene geometry before exporting the model from Blender. The shadow information is baked into each vertex as a per-vertex color. Sadly Unity does only support a single color per vertex. If this was not the case, I would also have baked static lighting into each vertex. Dynamic shadows are not computed, as the PS1 was not able to compute these.

Vertex snapping and perspective correction

Vertex positions are snapped to positions in screen space to make the move back and forth in distinct intervals. This emulates the limited accuracy used by the PS1 when computing vertex positions.

Perspective correction is not performed by the PS1 hardware (likely because it would require a division for each transformed position each frame). This behavior is emulated, performing the correction and reversing it later on. As a result textures close to the camera appear unnaturally stretched. This is also the likely reason for tessellation support on the PS1, to make this effect less noticeable.

Outline shader

To highlight items in a scene, each item is rendered by using a specialized outline shader. The shader add an extra outline pass, in which a model's faces are moved away from the model's center and scaled up. These faces are then rendered in a bright color for highlighting. Better outline shader variants exist, but this one was easy to implement, and the resulting artifacts are not visible at such low resolutions.

I implemented a dedicated shader for dropdown shadows, as dynamic shadows are not used. In essence, the game renders a transparent black texture below the player model, to give the illusion of a moving shadow. This helps significantly with depth perception.

Tesselation

I really wanted to implement tessellation of quad based geometry, as the PS1 also supported this feature. The easy and less performant way would be to use a script to dynamically tessellate a mesh at runtime, but I knew that GPUs supported hardware accelerated tessellation for quite a long time now. Unfortunately, Unity did not seem to like my tessellation shader code and it frequently crashed my OpenGL drivers. I put quite a lot of time and effort into getting tessellation to work, but was only partially successful. Now only Vulkan and DirectX 11/12 seem to run tessellation with a few issues. Metal and OpenGL support is practically broken and will even crash the Unity editor with no apparent reason.

Modeling, Texturing and Scenes

PS1 style low poly models are quite readily available on sites like itch.io, but I really wanted to create custom level and scene geometry from scratch. In the past I already worked with Blender to create some low quality models, but creating a whole village and an animated character model was quite a leap for me and my skills.

I started off by creating a simple demo scene with some uneven terrain and custom textures. For texturing I mostly relied on images I took myself, but I also used quite a lot of images from online sources. As most images were down scaled to dimensions of 32x32, most detail is lost, but that was the aesthetic we aimed for. Related textures were packed into 256x256 texture atlases. This enabled for efficient rendering and reuse of materials, as a single material could be used to texture all terrain elements like grass, gravel, pavement, etc. Using few materials also results in higher performance, as fewer draw calls are required, but performance with such low quality images and models was never really an issue. To play the correct sound when walking on grass, we created a custom lookup texture, that mapped sub-textures to their corresponding sounds.

The scene models are comparable to stage sets used in movies. Most walls are one-sided and create only the illusion of a wall. A scene's terrain consists of multiple 2 meter by 2 meter tiles, each textured with a 32x32 texture. Modeling terrain like this makes creating environments easier and is also the technique used by Silent Hill. For flat terrain creating the corresponding collision and navigation meshes is really easy, but non-uniform terrain like in the first scene of the game, is a challenge, as most vertices were placed by hand. I am sure there are tools out there for creating PS1 like terrain with less effort, but I was familiar with Blender and therefore used it exclusively.

Pathfinding

One of the project's goals was to implement some sort of enemy, that can harm the player character. For this to work, enemies needed to be able to move towards the player character. As Unity already supports navigation meshes and navigation agents I thought it would be really easy to implement pathfinding for our enemies. I should have known, that nothing comes easy in Unity.

Unity does support baking a navigation mesh from a given model's mesh, but for higher accuracy regarding rounded surfaces and stairs I really wanted to use a custom navigation mesh I created while modeling the scenes in Blender. Well, Unity does not seem to support custom navigation meshes in their implementation. Fortunately, I was able to develop custom pathfinding, that is able to parse our custom navigation meshes. The implementation utilizes the A* pathfinding algorithm to generate a path along the vertices in our custom navigation meshes by converting the mesh to a node graph. Sadly the computed paths by A* are jagged and not optimally short. This issue could be minimized by using more detailed navigation meshes, but this is only feasible up to a point. Post-processing on a path is better scalable and requires only a very simple navigation mesh. In our implementation the post-processing of paths is handled by the Simple Stupid Funnel Algorithm, which is able to generate optimal paths even form very stretched navigation meshes. The algorithm is indeed very clever and simple, but implementing it was a challenge regardless. Problems like different mesh scales and different coordinate systems (Y-Axis and Z-Axis were flipped) made the implementation all the more challenging. In the end I was victorious and the current implementation is not only sufficiently fast, but also only breaks in very rare instances.

What I have learned

Unity is relatively easy to work with when staying within pre-defined boundaries. When developing features like custom lighting, Unity most often than not stands in the way of the user. But even with the encountered issues, developing a game in Unity is probably way faster than developing a game in a custom game engine. That said, I will likely never develop a game in Unity again, if it is required to a PS1 like aesthetic. In these niche cases, a custom engine may be the better choice.

Sources

The game's source code is publically available here at my GitHub. Models and other content I created for the project may be found here over at itch.io. Everything is published under a permissive open source license.

Image Gallery

Ominous Street

Ominous Street


The Village

The Village


Inspecting an item

Inspecting an item


Last update on March 20, 2024

Luca Anthony Schwarz

lucaanthonyschwarz@googlemail.com