Dalichrome
banner
index.dalichro.me.ap.brid.gy
Dalichrome
@index.dalichro.me.ap.brid.gy
Projects and process: indie games, pixel art, figure drawing, and software—documented in fog-tinted pixels

🌉 bridged from https://dalichro.me/ on the fediverse by https://fed.brid.gy/
The Best Modular Character Workflow With Aseprite
Hi everyone, this blog is a little different from the rest, this is a **tutorial** post. Meaning that if you have no interest in implementing a modular character system using Aseprite, then you can skip this one, I won't judge you. ### What is a Modular Character System? It is a system in games where a character does not exist as a single graphic, but as many separate graphics or modules. In this blog, we'll specifically be using a humanoid character and look at how we can visually 'equip' different clothing, glasses, and even hair styles using Unity and Aseprite. By the end of this tutorial, you'll be able to swap helmets, hairstyles, and clothing on a single character rig with minimal code. And of course there are many many other use cases for this workflow, it doesn't need to be a character you use. **Modular** is the key word here, not character. With that said let me lay down the Aseprite basics. ### How to setup Aseprite In aseprite, you need to make sure every animation is a different tag and each 'module' that you want to trigger on and off is a layer. Take a look at this image. The reason some of the layers are missing frames on the right side is that I ended up flipping the player in this game instead of using those animations This is a screen capture of the aseprite file I made for the character in my most recent game jam submission. You can see each layer is either the character itself (Main) or some graphic that you'd want equipped. This is exactly how you want to make your aseprite file so that this workflow will actually work. Also it is important to keep those layer names nice and legible as you'll see, we need to use those later. ### In Unity First of all you need to go into your package manager window and make sure that you have Aseprite Importer installed, don't worry this is a package made and distributed by Unity themselves. After that, just drag your '.aseprite' file into a Unity folder and check it's import settings. So this is not the default unity view, but you can see I have clicked my _PlayerFrames.aseprite_ in the project view and now in the inspector I can see its import settings. What's important for us here is that we have '_Individual Layers_ ' enabled and '_Animation Clips_ ' enabled, then press that '_Export Animation Assets_ ' button and export the controller somewhere in your project. That controller is independent of the aseprite file, so if you delete the _whatever.aseprite_ file, the controller will stay around. The animation clips are a different story. If you export them, they will not change with the aseprite file anymore, so I recommend not doing that and using the ones the importer automatically creates (like Attack-Right in the project view of the above image). Also, this tutorial is not about setting up an animation controller, but you will need to have one working and setup for use in this tutorial, so make sure you set up the one you just exported. With that done you can now click on one of those clips and you should see something like this in the Animation tab: This can be a little confusing if you've never worked too in depth with the Unity Animation tab before, but Unity is now expecting different sprite renderers for each layer/module in the aseprite file. Specifically, it wants a sprite renderer named the same as the layer childed to object that has the animator component. So the 'TraumaCharacterModel' has no sprite renderer, but its childed objects 'Main', 'Torso', 'Hair', 'Beard', and 'Glasses' all do. Now with this setup you have a zero code solution for modular characters in Unity, but there remains one big issue. Scalability, if you have 100 different modules, you need 100 different childed game objects, which feels obscene, because it is. So we need a little code. ### The Coding The solution to the scalability problem is simple. Rename a childed object to the layer you want to show and refresh the animator. If you do so you can reuse the same generic object for many different layers. Here is the simplest way I can express that in code: layerGameObject.name = layer.name; layerGameObject.SetActive(true); GetComponent<Animator>().Rebind(); Of course, that only applies to one layer and one object, which creates another problem. What if you want to show modules concurrently like a separate object for helmet and chest-piece? In order to do that we need multiple generic objects. First let's call these objects '_slots_ '. Each layer needs to be associated with a slot. We can make two simple classes and an enum for the data we need to assign in the inspector. public enum CosmeticSlotType { NA, Torso, Pants, Head, Eye, Chin, Feet } [Serializable] public class CosmeticSlot { public CosmeticSlotType slot; public SpriteRenderer spriteRenderer; } [Serializable] public class Cosmetic { public string name; public CosmeticSlotType slot; } Then we can make a _MonoBehaviour_ to attach to the player. public class PlayerCosmetics : MonoBehaviour { [SerializeField] private List<Cosmetic> cosmetics; [SerializeField] private List<CosmeticSlot> cosmeticSlots; ... Go back into Unity, attach the script, and then type each layer name from Aseprite carefully into the inspector. Once you've done that head back into the code and we can add some data structures. public class PlayerCosmetics : MonoBehaviour { [SerializeField] private List<Cosmetic> cosmetics; [SerializeField] private List<CosmeticSlot> cosmeticSlots; private Dictionary<CosmeticSlotType, CosmeticSlotInfo> slotInfoDict = new(); private Animator animator; private class CosmeticSlotInfo { public CosmeticSlotType slot; public List<Cosmetic> cosmetics; public SpriteRenderer renderer; public GameObject GameObject { get { return renderer.gameObject; } } public CosmeticSlotInfo(CosmeticSlotType slot) { this.slot = slot; cosmetics = new (); renderer = null; } public void AddCosmetic(Cosmetic cosmetic) { cosmetics.Add(cosmetic); } } private void Start() { animator = GetComponent<Animator>(); CreateCosmeticSlots(); } private void CreateCosmeticSlots() { slotInfoDict = new(); foreach (CosmeticSlotType slot in Enum.GetValues(typeof(CosmeticSlotType))) { slotInfoDict.Add(slot, new(slot)); } foreach (Cosmetic cosmetic in cosmetics) { if(slotInfoDict.ContainsKey(cosmetic.slot)) slotInfoDict[cosmetic.slot].AddCosmetic(cosmetic); } foreach (CosmeticSlot slot in cosmeticSlots) { CosmeticSlotInfo info = slotInfoDict[slot.slot]; info.renderer = slot.spriteRenderer; slot.spriteRenderer.sprite = null; } } ... Now we have a dictionary where we can put in the cosmetic slot type and get back the actual slot class. With that working, all we need to do is to receive a cosmetic class and... public void SetCosmetic(Cosmetic cosmetic) { CosmeticSlotInfo slot = slotInfoDict[cosmetic.slot]; slot.GameObject.name = cosmetic.name; animator.Rebind(); } Now we can easily swap between different layers in one slot! We can also define a lot of slots and have hundreds of animations per slot, but still only have a limited number of game objects! If you want to test it, try adding something like this to '_Awake_ ': private void Awake() { Cosmetic newCosmetic = new(); newCosmetic.name = "AsepriteLayerName"; newCosmetic.slot = CosmeticSlotType.Head; SetCosmetic(newCosmetic); } ## Conclusion I was really excited when I implemented this during a jam as I had no clue that modular character animation could be this easy! The code I've provided is way less than 100 lines and it gives you all you need for the simplest version of a working system. Of course there are more professional implementations than enums and dictionaries, like having your own scriptable object item class in your resources folder that you hook into instead. But, I'll leave that to you to figure out. So let me know if you guys end up using this in your games and sign up to my website if you want more of this stuff or look at some of my other blogs. Thanks! ## Sign up for Dalichrome Projects and process: indie games, pixel art, figure drawing, and software—documented in fog-tinted pixels join the cool crew Email sent! Check your inbox to complete your signup. You can unsubscribe, but don't
dalichro.me
October 5, 2025 at 7:54 PM
Making A Graph Based Random Generator
### Preface So recently I participated in the 2025 Game Maker's Toolkit Game Jam (link to the blog on that) and in preparation for that event, and for use in my in-progress game, I made some significant changes to my random generator package. Now as it stands I view this random generator as my technical _mangum opus_ , it's a complicated project and the architecture is the best I've ever designed. Although I was proud of it, prior to the changes I'm about to layout, it lacked several key features that would make it impossible to use in production. But before I get into why that is, since this is going to be a more technical post, I've decided to break it up section by section more cleanly so that readers can skip to different parts easily. So skip it if you want, I'll start with describing terminology you should know so that we can all be on the same page. ### Terms So first, let's go over the terms I use for the data structures that store the generated data. #### Data Storage Terms **Tile** - The smallest unit in a tile grid, it contains an x and y coordinate and a tile type for each tile layer. Think of it as representing a small square of terrain that can define anything from a square foot of sea to a square foot of mossy castle walls. **Tile Grid** - The actual grid that contains all the tiles. The grids in my generator can have variable width and height, but cannot stretch infinitely. **Map** - Is just another term for tile grid. **Generation** - Another name for a map or tile grid, something output by a generator. **Tile Type** - The actual data that defines what's in a tile. A tile type could be grass, or tree, or anything that can generate really. Each tile type is restricted to only being placed in a specific tile layer. **Tile Layer** - Inside each tile there are layers, such as ground, wall, object, and debug (these are the current layers I have). Imagine each layer like a 2D plane, each layer renders on top of the previous. Each tile only contains one tile type for each layer. #### Generation Operations Okay now that we have an idea of my implementation of the tile grid, let's talk about how we create them. **Generator** - The algorithm used to place or modify tiles on an existing tile grid. An example would a noise generator, which the user gives a tile i.e. grass, and it places it at a random amount, outputting something visually similar to a grassy TV static. **Grid Operation** - An algorithm used to do something with a tile grid. A generator is a grid operation, but as you will see later there are many types of grid operations that are not generators. What's important is to realize this is the umbrella term. **Tile Masking** - The process of including or excluding certain tiles types from being modified by generators. **Generation Config Stack** - In the old linear model of generation, this would be the information that told the random generator exactly what to generate. **Operation Config** - The set of variables that tweak specific parts of a grid operation. They are serializable and are used to save specific generation information. **Random Generator -** This is the short-hand version of 2D Random Terrain Generator, it refers to the entire program that makes random terrain. ## **Recap** So, as I said this blog is about an update to an existing package. That's right, in fact I've already used it in the same jam last year! So, I'm not making a new random generator, but I'm modify the same one. It's important to know that the random generator at this point existed in two different parts. One was the package that housed all the actual generation logic that existed to be added to different unity packages via the package manager. The second was a separate Unity UI project that had its own front end that allowed users to see, test, and modify their generations. Picture of the old UI, notice the 'stack' of configs on the left The UI application could also export certain generation data as json, which then you could load into other unity projects and just import config stacks that you had tested fairly easily. But this blog post is only about updating the package side of the project. ### **Problem** So that sounds pretty good, so what's the issue? The answer is pretty straightforward, it could not generate 'regions'. I didn't realize it when I was initially making the random generator, but regions are _insanely_ important. Just think about how regional terrain is in real-life, seas, hills, mountains, beaches, cliffs, forests, jungles, swamps, etc. Imagine you just had only one of those, and that was basically what my generator could handle. You could make any one of those 'biomes,' but only one. So, I don't think its an overstatement to say that a random generator without regions is missing the core feature that makes maps interesting or engaging the first place. Now the technical reason why I couldn't create regions was I was using a stacked based config system, that had each generator resolved sequentially in order. This meant there was no way to split off a section of the map and run different generators on that section. So in order to make a good random generator that could make interesting maps, I had to design a new system that could do this. ### **Design** So it might sound complicated, but the high-level design changes are actually really simple. I wanted to take the linear stack based config system and transition to a graph based config system. This is would allow for multiple branching paths of generation and enable 'regionality' in generation most importantly. But as the generator stood then, the only type of grid operations were generators, which take in a tile grid and spit out a different tile grid. So I needed several new grid operations in order to enable this _regional_ generation workflow. Firstly we need an operation called 'splitters' that split the tile grid into different regions. A good example of a splitter algorithm is the Voronoi algorithm, which without going into specifics creates very geometric cellular regions. A Voronoi diagram, given the set of black points, it can create these geometric regions Then we needed 'filters' these are a bit unique as they don't change tile grid directly, but instead take all the different 'splits' or different regions from the splitter then select a subset of those then apply them to the tile grid. This makes it so that the next generators can only modify the regions that the filter returned. Then finally are the 'joiners' that take different 'lines' of generators then join them together, think of like a 'git merge'. These lines of generators are the different generators that run off of a filtered region after a splitter then a filter, you can think of them as the region specific generators. But I needed more than just these new operations, I needed to define a graph structure as well to put these operations in and then have some sort of UI that allowed me to make and modify them easily. # **Execution** I thought the UI was the likeliest point of failure, so I started from there, thinking I could back off if I needed to. I knew coding a UI from scratch was out of the question, so I knew that I needed to find a perfect package that met all my needs. ## XNode Sometimes the solution just exists. The package I found, XNode, was the perfect fit. It's a open source package that allows you to extend their node and graph classes to leverage their built-in UI. After I found it, I started with verifying that I could actually use it, by making the class GeneratorNode that extended their node class and GeneratorGraph that extended their graph class. Not only did it work, but it worked much better than I expected, within a couple weeks I was able to define a graph where each node was a config for a grid operation in the XNode UI. I was even able to write abstract classes that handled most of the logic for every node tied to a grid operation, meaning that I only needed one class per operation type not for each implementation. The abstraction worked so well that any new operation, when its added, automatically gets full UI support - no code needed. [Picture of the XNode UI] It was a slim and fast solution and I couldn't have been more surprised. Now that the UI was complete, I need to actually implement the traversal of the graph I had just made. ## Traversal To actually generate a map, the random generator needs to move through generator graph and each operation must run in the correct order _(Not doing parallel generation yet)_. Well, what is the correct order? The most obvious answer is we need to move from the start of the graph to the end. So I added some start and end nodes to the graph. A linear graph would just be a line of generators connecting start to finish. So the most simple traversal is run each node in order from start to finish. But we want to really leverage branching paths, that's the whole point of this feature and this blog. So let's add a branching path to that. So take a moment and think logically, how we want to move through this? We have two branching paths, so the biggest difference now is we can't run left to right anymore. The nodes after the joiner have to run after all the generators to left of it run. The way I handled this was by thinking of joiners like sort of a video game water puzzle. So each time you get to the joiner, you bring a bucket of water and fill up the pit a little bit more. When the pit is full, you swim across easily. But where does the water come from? Well I like to think of splitters as something a little bit more complicated, imagine a ledge you can get down, but not back up that also has a well at the bottom. A splitter is just like that, the first time you get to one, you can cross no problem, nothing needed. Then you take a bucket of water from the well, so when you find a water puzzle / joiner you can fill it up. Of course you don't have enough water on one go to fill the joiner, but look at the joiner diagram again, you can grab a box from the joiner then come back and place the box, get more water. This exchange is the basic back and forth of the traversal logic. Going forward with buckets and going backwards with boxes. Eventually you'll be able to cross the joiner and all will be well. But this doesn't explain why you'd want to bring boxes to go back past a splitter in the first place. The reason you need to be able to do that is if you have multiple layers of splitters! Generation Graph on the left with multiple splitters, a resulting map on the right Yes, as you can see graphs can have more than one level of regions. You can split a region into smaller sub regions, so our traversal logic requires us to be able to handle any amount of splitters or joiners and any amount of sub regions. Now knowing that I think its pretty clear how you have to make your way back over a splitter at some point, to run a splitter further up. I also understand that these images aren't a stand in for code which is much more precise. So I'll clarify that each splitter needs _n_ boxes, _n_ being the number of filters that branch off of it. Also there is only _n_ buckets of water at each 'well' as well (_har har_). Now the algorithm I described is also basically just a different form of DFS (Depth First Search), so I could've described it that way as well, but I personally found the imagery resonated with me more. Also it's a bit more fun that way. ## Result Simply put, the introduction of XNode and graphs in conjunction with new node operations such as splitters, filters and joiners worked to enable the generation of maps with regions. To come full circle, the real world application of this is readily playable on my itch in the form of the recent GMTK 25 Jam Submission my team and I made. The graph that I ended up making for that game jam had over 70 different nodes and was 384 x 384 tiles large, a total of 147,456 per layer and with four layers per tile, a total of 589,824 different tile types generated in less than three seconds in a browser- proving the system could handle production-scale complexity under jam constraints. Having just looked at some other graphs in the traversal section, look at the behemoth we ended up using. So I was incredibly proud of not only getting this working, but having it work under such pressure. But as they say now is the youngest you'll ever be and now is the worst my random generator will ever be, as I also cannot wait to continue to push the random generator further and make even cooler maps. ### Future Plans So what's next? The first thing I am going to tackle is how layers work. I want users to be able to add their own layers, which I think requires, in a weird way, to make the generator 3D? As there if there is no set number of layers the z axis could stretch infinitely, so I think at that point the generator will be a 3D generator that's meant to be rendered in 2D. The main reason for this change is I need to add two new layers at least and I feel like its a good opportunity to look into the system and future proof it. Also if I do it right, I might be able to expand its use cases. After that I hope to add meta-data and structures to the generator. I'm sure one of those two will warrant another blog post, whenever I get to it. But anyways thank you so much for getting this far! This has probably been one of the largest pieces of writing I've ever done on one of the most complicated features I've ever implemented. I tried to keep it technical but mainly architectural, so let me know how I did in the comments. I hope you enjoyed it and see you next time!
dalichro.me
October 4, 2025 at 1:34 PM
Dalichrome's First Anniversary
So it's almost been one year since I launched this site in October of 2024 and to commemorate the occasion, I have been building up a lot of cool stuff to share all at the same time. ### Website Overhaul! The website has undergone a complete visual overhaul. I've done a whole bunch of art as well as spent many hours scrolling through 'inspect element' to bring you an experience that is hopefully unique and interesting, instead of looking like a rushed student project. I recreated the main menu screen and changed the navigation in mobile as well. The layout is similar other than those, but visually it's completely different. A lot more chains and roses than before, so please do check it out! ### The Newsletter Works Again Yes! I fixed the newsletter stuff, it had to do with some miscommunication between my bulk email sending service and digital ocean, where I host the site. That is all cleared up so hopefully you're reading this in your inbox! ### New Pixel Art Another part of the website changes was the pixel art page has had a big makeover. Now the pixel art is actually the right resolution, so instead of looking like shit, the art I put up there looks as intended. In celebration of this, I've redone a couple pieces in my newer style, specifically the lion and falcon pieces. Also now you can choose specific zoom values for the art so that you can get closer without compromising the aspect ratio. Check it out! ### New Figures I've been doing a lot of figure art over the past year, but I just hadn't been uploading them. So I took some time and uploaded my backlog, so now there's about seven new pieces of figure art. So if you like those, go check them out! ### New Blogs I've written two new blogs as well, not including this one. The first is about my experience with the Game Makers Toolkit game jam this year and what mistakes I made and what I learned as a team lead. That is already out, so go and read it. The second one is finished, but not uploaded yet. I'm uploading it in a week to drip feed the content a bit, so not to overwhelm people. That one is about the upgrading of my random generator to work with graphs, so that I can do _regional_ generation. I'm quite proud of both blogs and I've been trying to get better at making my technical writing approachable and not dry. So, even if you don't think you're the target demographic for this kind of writing, you might like it more than you think. ### New Game?! Well kind of? I submitted a game to the GMTK 25 game jam this year, so if you're into game jam games, go check it out. It didn't get the greatest reviews, but I patched some issues after the fact, so I think its at least a good way to spend 5-10 minutes. At the very least it'll put the newest blog into context. Here's the link. ### The Future So as you can imagine, it's taken me a while to make all of these changes and to put up all this new content to the site, but I'm not done. I have one more blog idea, I haven't yet written, about how to use Aseprite to create a really easy modular animation system workflow in Unity. Think of game characters that can equip and swap armor then the new armor is animated on top of the character animations, that's what I mean by modular animation system. It's a super common scenario, right? But during the jam, I created this really clean workflow that I haven't seen elsewhere on line, so I've been thinking about putting it on Medium then linking back to here to increase traffic. It'll be probably the most tutorial-esque of any of my posts so far, so it should be an interesting style change. Also I'm continuing my work on my long term game project a more fleshed out version of the game we submitted to GMTK last year. I'm sure that work will give me plenty more to write about. But in short, I'm just going to continue to make more art, code more cool things, then put it all up here on this website. So if you have any interest in anything that I do at all. Please sign up for the mailing list, even if just to boost my ego. Thanks! ## Sign up for Dalichrome Please I beg of you Sign Up Email sent! Check your inbox to complete your signup. No spam. Unsubscribe maybe. There's an unsubscribe button, but don't press it
dalichro.me
September 28, 2025 at 9:01 PM