Since Novantica is set in a futuristic city, I always wanted to make sure the game had a certain level of city life – people walking around, bots zooming by, drones buzzing, trams gliding, and bikes rolling along. I hadn't done much 3D modeling before learning Unity, and, while modeling props, buildings, and other blockish things didn't seem to difficult, I struggled with creating humanoid characters. I knew that I wanted something low-poly, simple, and a bit cartoonish for the game's characters, but I'll admit that character design is probably not my strong suit.
I watched a lot of video tutorials and studied characters from other games, and I settled on a general style that I felt was good enough, but wanted to polish the character artwork before releasing the game. I also wanted to improve upon the diversity and variety, while giving the NPCs just a little bit more personality.
The existing NPCs were just a set of different models, with different hair styles, body shapes, and clothing. As you walked through the city, you'd inevitably encounter the same model over and over again. I could explain it away in the game by saying that the story involves time travel, but I wanted more random variety of the NPCs populating the city. The best way I could think of achieving this was by modularizing the characters so that I could randomly generate any number of NPCs using primitive parts and different color palettes.
I started with rebuilding the core characters in Blender to try to nail down an approach and style that fit better with the feel of the game. Once I was happy with the general look and feel, I started building out modular characters based on this style. Starting with a base body type, I added multiple different models for hair styles, tops, bottoms, shoes, and accessories like sunglasses and hats.
For coloring the models, I created several new textures that are UV mapped based on skin, hair, and colors for the tops, bottoms, shoes, and accessories. This way I could swap the texture on the model to change the complexion and colors of the outfits independent from the parts.
Once I had a decent number of parts, I exported all the parts together as a single FBX file, which I uploaded to Mixamo to rig for the game.
I brought the rigged model into Unity and created a material for the new textures. In Unity, each modular part is a separate game object within the model, and I wrote a script to randomize the different part shown for each section of the model, i.e. hair, top, bottom, shoes, and accessory.
ModularCharacter script randomizes which part to show for each section, and the
ModularColors script randomizes which texture to use for the whole model.
Although I ended up not using it, I also had an array of rules to ensure that specific tops would always match their bottoms for things like aprons and suits.
By randomizing the parts of each section along with the colors used in the textures and the scale of the model, this gave me a huge variety of different possibilities for each NPC's prefab instance.
Modular NPC extras – WIP pic.twitter.com/Oo70RK4w8d— Brent Jackson (@jxnblk) September 1, 2023
At this point, I was quite pleased with the results.
I continued by adding another body shape and adding blend shapes to the models and scripts and started replacing the prefabs used in the
Crowd script that randomly places the NPCs near the character and gives them a destination to walk to.
The results felt a lot better in the game, and it was much more difficult to spot "clones" of other characters with this much variety.
At this point, and if you know much about Unity, you can probably guess what happened next: the FPS tanked. Rendering a crowd of animated NPCs can already be taxing, and with this new modular character approach, each prefab now had multiple Skinned Mesh Renderers to draw which caused the CPU usage to spike.
I was still happy with the look of these new characters and wasn't about to roll back all these changes, so I started experimenting with ways to make this work in the game without killing the performance. I'll explain what I did in the next devlog post, so stay tuned...