commit 7d124a10f083fb6a3d4aa24f27c46d8bbec6a960 Author: kitsunecafe Date: Wed Oct 16 01:25:29 2024 -0500 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30b9507 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist/ + diff --git a/content/about.md b/content/about.md new file mode 100644 index 0000000..53c1829 --- /dev/null +++ b/content/about.md @@ -0,0 +1,17 @@ +{% extends "../layouts/page.html" %} +{% import "../layouts/macros.html" as macros %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block content %} +{{ macros::message(text="i'm rowan. my pronouns are she/her and it/its.", classes="right", avatar="/static/img/pfp64.webp") }} +{{ macros::message(text="i'm a creator and artist. i make music, stories, video games and use any medium that seems interesting to me.", classes="right", avatar="/static/img/pfp64.webp") }} +{{ macros::message(text="my two main projects are immersive sims set in a near-future scifi setting. eventually, i'll provide public information about these two games. i'm disabled and easily discouraged so my work is slow.", classes="right", avatar="/static/img/pfp64.webp") }} +{{ macros::message(text="other things i like are ", classes="right", avatar="/static/img/pfp64.webp") }} +{{ macros::message(text="okay but when are you releasing a new game", reversed=false, classes="accent left", avatar="/static/img/anon.webp") }} +{{ macros::message(text=" ", classes="right", avatar="/static/img/pfp64.webp") }} +{% endblock %} + diff --git a/content/blog/45dr-postmortem/index.md b/content/blog/45dr-postmortem/index.md new file mode 100644 index 0000000..3bc88dd --- /dev/null +++ b/content/blog/45dr-postmortem/index.md @@ -0,0 +1,34 @@ +{% extends "../../../layouts/post.html" %} + +{% block article %} +# 45 Day Rogulike Postmortem + +I worked on a Roguelike game from 8 March 2022 to 22 April 2022. My motivations were to teach myself new things, develop my existing skills, and have fun with the development process. Two out of three isn't bad. [Give it a try.](https://mochancrimthann.itch.io/45dr) + +Starting a new project is always exciting, so I put in many hours in the first couple of weeks. There's no shortage of inspiring roguelike games, but most of my inspiration was directly from [DCSS](https://crawl.develz.org/). I completed procedural generation, movement, tile visibility, combat, items, enemy AI, and a character controller within the first week and a half. Most of the development was straightforward to reason. I decided early in development to create a new component for each feature to avoid breaking previous ones. Nothing drains my motivation like a week-long bug-fixing marathon. + +My method of generating a dungeon level was disappointingly simple: [binary space partitioning](https://en.wikipedia.org/wiki/Binary_space_partitioning). BSP will determine whether a space can be split in half and then randomly split it vertically or horizontally. Repeat this until the space is sufficiently divided, determine how much of that space to use, then connect it to others with corridors. I separated the logic for generating the space of the dungeon and displaying the dungeon into two separate components: Dungeon Generator and Dungeon Renderer. The renderer is responsible for waiting for the generator to finish and then rendering the tiles onto a Unity tilemap. + +The field of view (FOV) system is simple as well. This system uses another tilemap for rendering visibility. Each tile initializes to black with full opacity (`RGBA(0, 0, 0, 1)`). The opacity value will change to 0.5 or 0 depending on visibility. + +![Tile opacity values](./values.png) + +When the target entity's FOV updates each turn, the visibility tilemap checks a target's FOV. I used a C# `HashSet` of coordinates to represent each entity's FOV. The visibility system monitors a target's FOV and sets each visible tile's opacity to 0. Previously visible tiles' opacities are then set to 0.5. Determining which tiles were previously visible is as simple as taking the difference between the last turn's visible tiles and this turn's visible tiles (Previous - Current). + +![Venn diagram of opacity values](./opacity-venn.png) + +Once the dungeon finishes generating, all "decorators" are notified and begin placing objects through the dungeon. Decorators include the player, enemies, stairs, and treasure chests. These components became a dumping ground for hacky code and workarounds, so I tried to ensure this code didn't influence any other systems. + +Procedural item generation took far and away the most amount of time for a disproportionately small payoff. The base item is simple: it's just a list of attributes. Any system which interacts with items queries this list. Moving all the behavior out of the items and into relevant systems allows for more contextual behavior. The idea is that items were naive containers of data, not unlike an ECS entity, and different systems would handle items differently. I wish I had adopted more of an ECS approach with how effective the items were. + +As an example of the item and attribute system, I'll use `DamageAttribute`. It extends a base attribute called `ItemAttribute` which has a few overridable properties: `Label`, `EditorDescription`, and `Description`. It also has a string value to accept a [dice notation](https://en.wikipedia.org/wiki/Dice_notation) value representing the damage. The string gets parsed into a custom `Dice` object. The combat system queries all relevant items from the attacker and defender (namely, the attacker's weapon(s) and the defender's armor(s)). It rolls the attacker's hit value versus the defender's armor class (AC) to determine whether the hit connects. If the hit connects, it rolls all damage dice (by querying every item's `DamageAttribute`). Once the damage is determined, it queries the defender's highest level `IDamageHandler` and tells it to apply the damage. `IDamageHandler` is an interface that allows multiple sources of defense to handle incoming damage in a layered way (for example, AC -> elemental resistances -> physical resistances -> health). + +I may have overengineered it a bit given how small the game is, but working with items and their attributes was very easy and let me develop complex systems around them with very little friction. The biggest time-sink was getting Unity to cooperate with these objects. This approach *requires* custom editor scripts to use their maximum effectiveness. Writing editor scripts for a small game isn't exactly how I wanted to spend my time. I could have probably used [Unity's DOTS](https://unity.com/dots) framework, but I've struggled with that before and didn't want to try again. I've made and used ECS frameworks before, but Unity's strikes me as needlessly complicated. + +The UI was rushed at times and required me to rewrite it a few times. Thankfully it was kept minimal, so most of the rewrites didn't take much longer than half an hour. I think I'll pick up a UI framework and use that in the future. + +To my surprise, the most common question and feedback I received was about the "(worn)" tag that's affixed to equipped items. Many people thought it was related to a durability system which makes sense in retrospect -- you can't "wear" a sword. The reason for using "worn" rather than "equipped" was that I'd frequently seen it used in this context, and I wanted to maximize character space for item names. Thanks to everyone who caught this -- it is small but valuable feedback. + +It's too early to say if I want to come back and work on this game. I learned a lot from this experience, and I'm glad I did it. I'm thinking of writing articles on exactly how I implemented certain aspects of the game, so let me know if this would be valuable to you. +{% endblock article %} + diff --git a/content/blog/45dr-postmortem/index.toml b/content/blog/45dr-postmortem/index.toml new file mode 100644 index 0000000..d02947b --- /dev/null +++ b/content/blog/45dr-postmortem/index.toml @@ -0,0 +1,3 @@ +title = "45 Day Roguelike Postmortem" +created_date = "2022-04-22" + diff --git a/content/blog/45dr-postmortem/opacity-venn.png b/content/blog/45dr-postmortem/opacity-venn.png new file mode 100755 index 0000000..61ee013 Binary files /dev/null and b/content/blog/45dr-postmortem/opacity-venn.png differ diff --git a/content/blog/45dr-postmortem/values.png b/content/blog/45dr-postmortem/values.png new file mode 100755 index 0000000..e439a67 Binary files /dev/null and b/content/blog/45dr-postmortem/values.png differ diff --git a/content/blog/a-roxy-update/index.md b/content/blog/a-roxy-update/index.md new file mode 100644 index 0000000..665d72e --- /dev/null +++ b/content/blog/a-roxy-update/index.md @@ -0,0 +1,30 @@ +{% extends "../../../layouts/post.html" %} + +{% block article %} +# A Roxy Update + +I released a new version of Roxy over at [fem.mint.lgbt](https://fem.mint.lgbt/kitsunecafe/roxy-cli). I've been working on it for a few months now, one part because it's more complex than the original, and another part because I had to learn a lot. I want to make a quick rundown of the changes. + +## Philosophy +My philosophy about how Roxy should be structured changed as I built V1. The original was focused on simplicity and ease of implementation. Unfortunately, because I didn't account for every feature I wanted from the very beginning, it quickly went from easy to difficult and required a bunch of hacks to finish. + +Roxy's new philosophy is about pluggable behavior. The core is effectively a reducer for anything which `impl Read`. It's still not the most elegant and there's a *lot* I've learned from this exercise, I think it's a lot better. It's now possible to easily add new functions to Roxy by adding a new `Parser`. + +```rs + let parser = Parser::new(); + + let md_parser = MarkdownParser::new(); + parser.push(md_parser); + + let html_parser = HtmlParser::new(); + parser.push(html_parser); + + Roxy::process_file("input-file.md", "output-file.html", &mut parser); +``` + +Hopefully this lets me manage the complexity for whenever I want to add a new feature or parsing ability to the library. A lot of the comlexity still exists in one place, `roxy-cli`, but I'll hopefully break that down over time. + +## What's left +There's a bunch of stuff I didn't get to do. Two things I really want are an RSS feed generator, and streaming file inputs. Right now, the library loads the entire file into memory and works on it when it should be streaming it and processing it in chunks. I didn't have the time or energy to work on that, but maybe later. +{% endblock article %} + diff --git a/content/blog/a-roxy-update/index.toml b/content/blog/a-roxy-update/index.toml new file mode 100644 index 0000000..2ea93b8 --- /dev/null +++ b/content/blog/a-roxy-update/index.toml @@ -0,0 +1,3 @@ +title = "A Roxy Update" +created_date = "2024-02-14" + diff --git a/content/blog/convenient-unity-attributes/header-example.png b/content/blog/convenient-unity-attributes/header-example.png new file mode 100755 index 0000000..573a981 Binary files /dev/null and b/content/blog/convenient-unity-attributes/header-example.png differ diff --git a/content/blog/convenient-unity-attributes/hideininspector-example.png b/content/blog/convenient-unity-attributes/hideininspector-example.png new file mode 100755 index 0000000..1f588ad Binary files /dev/null and b/content/blog/convenient-unity-attributes/hideininspector-example.png differ diff --git a/content/blog/convenient-unity-attributes/index.md b/content/blog/convenient-unity-attributes/index.md new file mode 100644 index 0000000..de50881 --- /dev/null +++ b/content/blog/convenient-unity-attributes/index.md @@ -0,0 +1,230 @@ +{% extends "../../../layouts/post.html" %} + +{% block article %} +# Convenient Unity Attributes + +Unity offers a range of convenient ways to manipulate and hack its inspector. Unfortunately, thanks to the size of Unity's documentation, these types of methods go unnoticed. + +I won't attempt to iterate every useful attribute in Unity's library. I encourage you to explore Unity's documentation. + +# Header + +[Documentation](https://docs.unity3d.com/2022.1/Documentation/ScriptReference/HeaderAttribute.html) + +This simple and effective attribute will display a header above the property to which it is applied. + +```cs +public class Movement : MonoBehaviour +{ + [Header("Configuration")] + public float MaxSpeed = 5f; + public float MaxAcceleration = 5f; + + [Header("Dependencies")] + public Rigidbody body; + public Camera cam; +} +``` + +![Example of the inspector](./header-example.png) + +# Space + +[Documentation](https://docs.unity3d.com/2022.1/Documentation/ScriptReference/SpaceAttribute.html) + +The `Space` attribute adds user-defined pixel spacing between fields. I don't often reach for this attribute, but it can be handy in cases where a `Header` isn't appropriate. It helps designers scan through fields by creating a small visual break. + +Unity's documentation illustrates a good use case (with some modifications). + +```cs +public class Example : MonoBehaviour +{ + [SerializeField] private int health = 0; + [SerializeField] private int maxHealth = 100; + + [Space(10)] // 10 pixels of spacing here. + + [SerializeField] private int shield = 0; + [SerializeField] private int maxShield = 0; +} +``` + +A 10-pixel space is between `maxHealth` and `shield`. + +![Screenshot of example component](./space-example.png) + + +# Tooltip + +[Documentation](https://docs.unity3d.com/2022.1/Documentation/ScriptReference/TooltipAttribute.html) + +Unsurprisingly, the `Tooltip` attribute creates a tooltip over a field when hovered. The use case for these types of in-engine documentation is nearly endless: providing context, usage hints, and so much more. + +I won't use Unity's example because `Range` is better suited for that use case. We'll cover that attribute next. + +```cs +public class Movement : MonoBehaviour +{ + [SerializeField] + [Tooltip("Measured in meter/sec")] + private float MaxSpeed = 5f; +} +``` + +![Example of tooltip attribute](./tooltip-example.png) + +Although in this particular case, I may suggest a custom attribute that appends "m/s" to the end of the field, a tooltip can provide this type of clarity and context. + +# Range + +[Documentation](https://docs.unity3d.com/2022.1/Documentation/ScriptReference/RangeAttribute.html) + +We'll revisit Unity's `Tooltip` example with a different attribute: `Range`. + +```cs +public class Example : MonoBehaviour +{ + [Tooltip("Health value between 0 and 100.")] + int health = 0; +} +``` + +This component wants to restrict `health` between 0 and 100. Rather than enforcing that restriction through code, it relies on the designer to abide by this restriction. Let's correct it using `Range`. + +```cs +public class Example : MonoBehaviour +{ + [SerializeField, Range(0, 100)] + private int health = 0; +} +``` + +![Example of Range attribute](./range-example.png) + +The inspector is now enforcing the restriction and displaying it as a slider between the two values. I find this helps reason about the range better than a number field. + + +# SerializeField + +[Documentation](https://docs.unity3d.com/2022.1/Documentation/ScriptReference/SerializeField.html) + +Of all the attributes offered by Unity, this one sees the most use in my code. This attribute forces Unity's inspector to serialize and display the field in the inspector *regardless of accessibility*. This pattern is particularly effective for exposing values in the inspector while keeping them inaccessible to the codebase. + +Consider this simple example. + +```cs +public class Health: MonoBehaviour +{ + public int Health = 5; + public UnityEvent Damaged; + + public void TakeDamage(int damage) + { + if (damage > 0) + { + Damaged?.Invoke(); + Health = Mathf.Max(Health - damage, 0); + } + } +} +``` + +Setting `Health` to `public` allows a designer to set the initial health and tweak it during gameplay. These types of considerations are great for playtesting and debugging. Unfortunately, *anything* can modify `Health` without calling `TakeDamage(int)` which, can introduce an undesirable side effect: the `Damaged` event won't fire. + +It's possible to mitigate this issue by using a C# property. + +```cs +public class Health : MonoBehaviour +{ + private int health = 5; + public int Health + { + get => health; + set + { + if (value < health) + { + Damaged?.Invoke(); + } + + health = value; + } + } + + public UnityEvent Damaged; +} +``` + +It's now impossible to modify `Health` without firing `Damaged`. However, by default, Unity does **not** render C# properties in the inspector. A designer wanting to set the initial health or tweak the health value during playtesting will be unable. Let's try using `SerializeField` instead. + +```cs +public class Health : MonoBehaviour +{ + [SerializeField] private int health = 5; + public UnityEvent Damaged; + + public void TakeDamage(int damage) + { + if (damage > 0) + { + Damaged?.Invoke(); + health = Mathf.Max(health - damage, 0); + } + } +} +```` + +`health` will be editable in the inspector, but only `TakeDamage` will be accessible to code. It's also possible to combine the `property` and `SerializeField` approaches by exposing a private backing field to the inspector. + +# HideInInspector + +[Documentation](https://docs.unity3d.com/2022.1/Documentation/ScriptReference/HideInInspector.html) + +This attribute acts as the reverse of `SerializeField`: this will hide a `public` field from the inspector. It is helpful for hiding complexities from a designer without rendering it inaccessible to the rest of the code. + +Let's hide some component dependencies without making them inaccessible. + +```cs +[RequireComponent(typeof(Rigidbody), typeof(CapsuleCollider))] +public class Movement : MonoBehaviour +{ + [HideInInspector] public Rigidbody Body; + [HideInInspector] public CapsuleCollider Collider; + + private void Start() + { + Body = GetComponent(); + Collider = GetComponent(); + } +} +``` + +![Example of HideInInspector attribute](./hideininspector-example.png) + + +# RequireComponent + +[Documentation](https://docs.unity3d.com/2022.1/Documentation/ScriptReference/RequireComponent.html) + +This attribute doesn't neatly fit into this list, although I'm sure it's important enough to discuss. `RequireComponent` ensures that the specified component is attached to a GameObject. It's great for wrapping/enhancing built-in components or just assuring the existence of a dependency. + +`RequireComponent` will automatically add any required components which are missing. It will also prevent removing them while using the inspector. It's added to the `MonoBehaviour` rather than a field. + +```cs +[RequireComponent(typeof(Rigidbody))] +public class Movement : MonoBehaviour +{ + private Rigidbody body; + + private void Start() + { + body = GetComponent(); + } +} +``` + +Unity adds a `Rigidbody` if it doesn't already exist on the GameObject. Attempting to remove `Rigidbody` will cause an error. + +![Screenshot of the error message](./remove-requiredcomponent.png) +{% endblock article %} + diff --git a/content/blog/convenient-unity-attributes/index.toml b/content/blog/convenient-unity-attributes/index.toml new file mode 100644 index 0000000..55e6b2f --- /dev/null +++ b/content/blog/convenient-unity-attributes/index.toml @@ -0,0 +1,3 @@ +title = "Convenient Unity Attributes" +created_date = "2021-12-09" + diff --git a/content/blog/convenient-unity-attributes/range-example.png b/content/blog/convenient-unity-attributes/range-example.png new file mode 100755 index 0000000..9fa9928 Binary files /dev/null and b/content/blog/convenient-unity-attributes/range-example.png differ diff --git a/content/blog/convenient-unity-attributes/remove-requiredcomponent.png b/content/blog/convenient-unity-attributes/remove-requiredcomponent.png new file mode 100755 index 0000000..818f6d1 Binary files /dev/null and b/content/blog/convenient-unity-attributes/remove-requiredcomponent.png differ diff --git a/content/blog/convenient-unity-attributes/space-example.png b/content/blog/convenient-unity-attributes/space-example.png new file mode 100755 index 0000000..609654e Binary files /dev/null and b/content/blog/convenient-unity-attributes/space-example.png differ diff --git a/content/blog/convenient-unity-attributes/tooltip-example.png b/content/blog/convenient-unity-attributes/tooltip-example.png new file mode 100755 index 0000000..1c8e114 Binary files /dev/null and b/content/blog/convenient-unity-attributes/tooltip-example.png differ diff --git a/content/blog/index.md b/content/blog/index.md new file mode 100644 index 0000000..51247f8 --- /dev/null +++ b/content/blog/index.md @@ -0,0 +1,17 @@ +{% extends "../../layouts/page.html" %} + +{% block content %} +
    + {% for post in blog + | values + | filter(attribute="created_date") + | sort(attribute="created_date") + | reverse %} +
  • + {{ post.title }} + {{ post.created_date | date(format="%b %e, %Y") }} +
  • + {% endfor %} +
+{% endblock content %} + diff --git a/content/blog/index.toml b/content/blog/index.toml new file mode 100644 index 0000000..f38a157 --- /dev/null +++ b/content/blog/index.toml @@ -0,0 +1,3 @@ +title = "Blog" +index = true + diff --git a/content/blog/making-a-static-site-generator-also-a-new-site/index.md b/content/blog/making-a-static-site-generator-also-a-new-site/index.md new file mode 100644 index 0000000..47d3658 --- /dev/null +++ b/content/blog/making-a-static-site-generator-also-a-new-site/index.md @@ -0,0 +1,51 @@ +{% extends "../../../layouts/post.html" %} + +{% block article %} +# Making a Static Site Generator (Also a New Site) +I decided to give my site a much-needed redesign, so in the spirit of making things harder than they need to be, I decided to make a static site generator. You might see "Made with Roxy" at the bottom of this page (unless you're reading this so far in the future that I've changed it again). Roxy is what I've decided to call this generator. I wanted to write a little about my experiences creating it. + +## The Plan + +Initially, I planned to create *everything* from scratch -- the file reader, the markdown parser, the syntax highlighter, everything. After a few attempts, I decided that this was entirely silly and scrapped that idea. I also toyed with the idea of using a compile-time layout system but I didn't want to have to recompile a Rust executable every time I made changes to the layouts. + +I finally settled on using [Tera](https://tera.netlify.app) as my templating engine. It's Jinja-like and made for Rust with a lot of really useful features. It's significantly slower than compile-time engines but also much more flexible. In addition to a templating engine, I also needed something to parse Markdown files which would be the basis for the content of each page. For this, I used [pulldown-cmark](https://docs.rs/highlight-pulldown/latest/highlight_pulldown/). + +With these two things decided on, my idea was simple: layouts would be defined in one location and content would be defined in another. Most fancy layout stuff would be handled by Tera (includes, extensions, etc) and the data would live in the Markdown. This created two distinct types of data: `layouts` and `content`. I also had the idea that the `content` directory structure should define the site's structure as well. (eg. `content/blog/my-post.md` becomes `domain.com/blog/my-post`). + +## The "Implementation" + +Creating it was straightforward -- in fact, it was so simple that I accidentally deleted the project while trying to push it to git and had to rewrite it. I did it in a few hours (and stayed up until 6 am doing it). + +It only performs a few steps. + +1) Compile the content from the `content` directory + 1) If this filename is prefixed with `.` (dot), skip it + 2) Read front matter + 3) Parse the rest as markdown + 4) Parse the previous result as a one-off Tera template +2) Assemble the content into a `HashMap` + * This is so it can be easily accessed later + * There is a limitation in my implementation -- Roxy only compiles a map out of the root `content` directory. This will become relevant later. +3) Create Tera context + * This is where the content map becomes relevant. This `HashMap` is serialized and given to each layout. One major use would be to iterate their contents, eg. a blog index that displays all posts. Subdirectories can't be accessed if they aren't recognized as content themselves). +4) Create output files + 1) Iterate each content file + 2) Create a matching directory structure in the output folder (eg. `content/blog/my-post.md` becomes `output/blog/my-post/index.md`) + 3) If it has a `layout` frontmatter field, try to use that layout, otherwise, use `index.html` as a default + 4) Render the template using the context made earlier + 5) Write out the file +5) Copy static files to the output folder + +There were a few things I didn't consider in my original plan. Most notably, syntax highlighting and command-line argument parsing. I ended up adding `syntect` for syntax highlighting and `clap` for argument parsing. Additionally, there's a quirk based on my decision to tie `content` to page layout: to create a page, even if it's entirely layout driven, it requires a content file as a placeholder. + +There's a bunch of things Roxy *doesn't* do + +* Create a development web server +* Create new content files from a template +* Initialize a project to give the user an idea of how to use it + +I ended up making bash files to do all that though and they're available in the repository for this website if you'd like to see or use them. + +The code is a disaster and needs to be refactored. I'll do that the minute I have a really good reason to. For now, it works well enough for me, and I *really* like using it. This is one of the times I'm very happy I decided to make something superfluous for myself. +{% endblock article %} + diff --git a/content/blog/making-a-static-site-generator-also-a-new-site/index.toml b/content/blog/making-a-static-site-generator-also-a-new-site/index.toml new file mode 100644 index 0000000..c1ff4df --- /dev/null +++ b/content/blog/making-a-static-site-generator-also-a-new-site/index.toml @@ -0,0 +1,2 @@ +title = "Making a Static Site Generator (and a New Site)" +created_date = "2023-08-15" diff --git a/content/blog/simplifying-code-with-components/index.md b/content/blog/simplifying-code-with-components/index.md new file mode 100644 index 0000000..0286030 --- /dev/null +++ b/content/blog/simplifying-code-with-components/index.md @@ -0,0 +1,204 @@ +{% extends "../../../layouts/post.html" %} + +{% block article %} +# Simplifying Code with Components + +Unity is a component-based game engine. Without any context, "component" is a nebulous and vague term. Understanding what a component *is* becomes crucial to understanding Unity. In Unity's [Introduction to components](https://docs.unity3d.com/Manual/Components.html), they provide an unsatisfactory definition of a component. + +> Components define the behaviour of that GameObject. + +While this is entirely accurate, especially within the context of Unity, it fails to provide a good idea of the fundamental concepts behind components. + +# What is a component? + +[Game Programming Pattern](https://gameprogrammingpatterns.com/component.html)'s definition provides more insight into the purpose of a component. + +> Allow a single entity to span multiple domains without coupling the domains to each other. + +Without reading further into the article, I find the definition sterile and hard to visualize. That said, I would highly recommend reading the rest article and the entirety of the book - it's worth it! + +I'll make an effort to define a component as well. + +> A component is a single, reusable domain behavior with an intuitive interface. + +As much as possible, a component should attempt to focus on a single behavior within a single domain. It should have a clear and simple interface to allow its entity, other entities, and other components to interact with its behavior. + +I find that Unity's [`CharacterController`](https://docs.unity3d.com/ScriptReference/CharacterController.html) is an example of a good component. It exposes two public methods, [`Move(Vector3)`](https://docs.unity3d.com/ScriptReference/CharacterController.Move.html) and [`SimpleMove(Vector3)`](https://docs.unity3d.com/ScriptReference/CharacterController.SimpleMove.html). + +`Move(Vector3 motion)` "supplies the movement of a GameObject with an attached CharacterController component." It takes the desired movement vector and attempts to displace the GameObject to which it is attached. It makes no assumptions about gravity since different games have different requirements for gravity. + +`SimpleMove(Vector3 speed)` "moves the character with speed." Speed is a movement vector -- a direction and speed to move per second. This method *does* make assumptions about gravity. + +`CharacterController` hides the complexity of ground collision, physics interactions, and movement interpolation. Additionally, the two methods hide or reveal certain complexities about how the component moves the character. Certain Unity-isms regarding Collision events notwithstanding, the interface is *very* simple and obvious. + + +# When should I create a new component? + +The obvious answer is whenever you need a new behavior. If you're unsure, it is always possible and encouraged to review and refactor code to identify the need for abstractions like a new component. + +Using movement as an example again, should character rotation be part of the character movement component? The two often occur simultaneously and even rely on one another. It makes sense to combine rotation and displacement into the same behavior. There are likely cases where this may not be true, and in that instance, the developer should split them into separate components. + +Should jumping be included in the movement component? I can imagine many scenarios where an entity would need to move but not jump. In those cases, it makes sense to separate them into different components. + +I obviously won't iterate every possible decision on whether or not I would personally separate a behavior into a new component. It's important to make thoughtful decisions about your code architecture, similar to the examples above. + +# Making a movement component + +Let's use Unity's [`CharacterController.Move`](https://docs.unity3d.com/ScriptReference/CharacterController.Move.html) example. + +```cs +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class Example : MonoBehaviour +{ + private CharacterController controller; + private Vector3 playerVelocity; + private bool groundedPlayer; + private float playerSpeed = 2.0f; + private float jumpHeight = 1.0f; + private float gravityValue = -9.81f; + + private void Start() + { + controller = gameObject.AddComponent(); + } + + void Update() + { + groundedPlayer = controller.isGrounded; + if (groundedPlayer && playerVelocity.y < 0) + { + playerVelocity.y = 0f; + } + + Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")); + controller.Move(move * Time.deltaTime * playerSpeed); + + if (move != Vector3.zero) + { + gameObject.transform.forward = move; + } + + // Changes the height position of the player.. + if (Input.GetButtonDown("Jump") && groundedPlayer) + { + playerVelocity.y += Mathf.Sqrt(jumpHeight * -3.0f * gravityValue); + } + + playerVelocity.y += gravityValue * Time.deltaTime; + controller.Move(playerVelocity * Time.deltaTime); + } +} +``` + +Although it's not relevant, this example goes against Unity's advice to not call `Move` or `SimpleMove` more than once per frame. + +One of the first things to notice about this component is that it crosses two domains: input and movement. Let's start by separating those concerns. + +## Separating concerns + +```cs +public class InputProvider : MonoBehaviour +{ + public Vector2 CurrentDirection { get; protected set; } + public bool WantsToJump { get; protected set; } + + private void Update() + { + CurrentDirection = new Vector2( + Input.GetAxis("Horizontal"), + Input.GetAxis("Vertical") + ); + + WantsToJump = Input.GetButton("Jump"); + } +} +``` + +It doesn't seem like much, but small components like this one help separate unrelated logic to make them more composable. +Let's refactor the remaining example code. + +We'll start by renaming the class and the file to `EntityMovement` and `EntityMovement.cs`. + +```cs +public class EntityMovement : MonoBehaviour +``` + +Now let's add a reference to our new `InputProvider` component. + +```cs +public class EntityMovement : MonoBehaviour +{ + private InputProvider input; + // ... + + private void Start() + { + //... + input = gameObject.GetComponent(); + } +``` + +With our reference to `InputProvider`, we can replace the existing `Input` references. + +```cs + private void Update() + { + // ... + Vector3 move = new Vector3(input.CurrentDirection.x, 0, input.CurrentDirection.y); + controller.Move(move * Time.deltaTime * playerSpeed); + + if (move != Vector3.zero) + { + gameObject.transform.forward = move; + } + + if (input.WantsToJump && groundedPlayer) + { + playerVelocity.y += Mathf.Sqrt(jumpHeight * -3.0f * gravityValue); + } + // .. + } +``` + +Create a sphere with the `EntityMovement` and `InputProvider` components and move it with WASD or the arrow keys. It should operate like Unity's `Example` component, but now we've separated input handling from movement! +Unfortunately, the changes we've made only illustrate the concept of splitting concerns without actually having done anything practical. What *would* be useful is if these components were usable by more than the player character. Let's refactor our code to make that possible. + +## Making it reusable + +For our components to be reusable, `InputProvider` needs to act as a generic interface rather than a thin proxy to `Input`. An easy way to accomplish this would be to refactor `InputProvider` as a C# `interface`. Thankfully Unity's `GetComponent` works with classes and interfaces. Be sure to remove `InputProvider` from the existing GameObject. + +```cs +public interface InputProvider +{ + Vector2 CurrentDirection { get; } + bool WantsToJump { get; } +} + +``` + +A new component is required to implement the interface, `PlayerInputProvider`. It will contain the implementation details that previously lived in `InputProvider`. + +```cs +public class PlayerInputProvider : MonoBehaviour, InputProvider +{ + public Vector2 CurrentDirection { get; protected set; } + public bool WantsToJump { get; protected set; } + + private void Update() + { + CurrentDirection = new Vector2( + Input.GetAxis("Horizontal"), + Input.GetAxis("Vertical") + ); + + WantsToJump = Input.GetButton("Jump"); + } +} +``` + +Attach `PlayerInputProvider` to the player object and ensure the character moves again. A future component could be `EnemyInputProvider` which determines movement based on a `NavMeshAgent`. +{% endblock article %} + diff --git a/content/blog/simplifying-code-with-components/index.toml b/content/blog/simplifying-code-with-components/index.toml new file mode 100644 index 0000000..d5c70bf --- /dev/null +++ b/content/blog/simplifying-code-with-components/index.toml @@ -0,0 +1,3 @@ +title = "Simplifying Code with Components" +created_date = "2021-12-05" + diff --git a/content/index.md b/content/index.md new file mode 100644 index 0000000..94e2a9e --- /dev/null +++ b/content/index.md @@ -0,0 +1,89 @@ +{% extends "../layouts/index.html" %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block body %} +{% set date_format = "%A, %d %B, %Y" %} +
+
+
+ +
+ + a smaller banner for this site + +
+
+
+
+

welcome!

+
+
+
+ +
+
+

i'm rowan: game dev, artist, musician, and creature of the moon.

+

this site is devoted to highlighting my works. the design of this site is focused on having a small footprint, minimal waste, and accessible design. i've done my best to support many different browers and versions but maximum compatibility requires a lot of extra work (and shims)

+

not including linked or hosted projects, this site has

+
    +
  • no javascript
  • +
  • few images
  • +
  • less than 128 kB per page (with most being under 30 kB)
  • +
+

not everything that i make has the same focus but links to those projects will be annotated with content warnings.

+
+
+
+
+

blog

+    +
+
    + {% for post in blog + | values + | filter(attribute="created_date") + | sort(attribute="created_date") + | reverse + | slice(end=5) %} +
  • + {{ post.title }} {{ post.created_date | date(format=date_format) }} +
  • + {% endfor %} +
+
+
+
+

projects

+    +
+
    + {% for project in projects + | values + | filter(attribute="created_date") + | sort(attribute="created_date") + | reverse + | slice(end=5) %} +
  • + {{ project.title }} {{ project.created_date | date(format=date_format) }} +
  • + {% endfor %} +
+
+
+
+ {% include "../layouts/footer.html" %} +
+{% endblock %} + diff --git a/content/projects/45dr.toml b/content/projects/45dr.toml new file mode 100644 index 0000000..cfd717a --- /dev/null +++ b/content/projects/45dr.toml @@ -0,0 +1,6 @@ +title = "45 Day Roguelike" +created_date = "2022-04-22T00:00:00Z" +type = ["game"] +link = "https://mochancrimthann.itch.io/45dr" +description = "A roguelike made in Unity from 8 March 2022 to 22 April 2022." + diff --git a/content/projects/blood-of-yamin.toml b/content/projects/blood-of-yamin.toml new file mode 100644 index 0000000..7e3a7c4 --- /dev/null +++ b/content/projects/blood-of-yamin.toml @@ -0,0 +1,6 @@ +title = "Blood of Yamin" +created_date = "2022-10-20T00:00:00Z" +type = ["game"] +link = "https://store.steampowered.com/app/1960250/Blood_of_Yamin/" +description = "A metroidvania RPG that I assisted with development." + diff --git a/content/projects/channel-terror.toml b/content/projects/channel-terror.toml new file mode 100644 index 0000000..74eef78 --- /dev/null +++ b/content/projects/channel-terror.toml @@ -0,0 +1,6 @@ +title = "Channel Terror" +created_date = "2021-02-11T00:00:00Z" +type = ["game"] +link = "https://thepancakewitch.itch.io/channel-terror" +description = "A small puzzle game made in 3 days for the r/SoloDevopment Halloween Jam." + diff --git a/content/projects/gourmand.toml b/content/projects/gourmand.toml new file mode 100644 index 0000000..de96793 --- /dev/null +++ b/content/projects/gourmand.toml @@ -0,0 +1,6 @@ +title = "Gourmand" +created_date = "2021-05-03T00:00:00Z" +type = ["game"] +link = "https://mochancrimthann.itch.io/gourmand" +description = "Made in 3 days for the SoloDevelopment Minimalism Jam." + diff --git a/content/projects/index.md b/content/projects/index.md new file mode 100644 index 0000000..aeb3620 --- /dev/null +++ b/content/projects/index.md @@ -0,0 +1,18 @@ +{% extends "../../layouts/page.html" %} + +{% block content %} +
    + {% for project in projects + | values + | filter(attribute="created_date") + | sort(attribute="created_date") + | reverse %} +
  • + {{ project.title }} + {{ project.created_date | date(format="%b %e, %Y") }} +

    {{ project.description }}

    +
  • + {% endfor %} +
+{% endblock content %} + diff --git a/content/projects/kitsucafe.md b/content/projects/kitsucafe.md new file mode 100644 index 0000000..e69de29 diff --git a/content/projects/kitsucafe.toml b/content/projects/kitsucafe.toml new file mode 100644 index 0000000..cc98c03 --- /dev/null +++ b/content/projects/kitsucafe.toml @@ -0,0 +1,6 @@ +title = "kitsu.cafe" +created_date = "2023-08-12T00:05:30Z" +type = ["site"] +link = "https://github.com/kitsunecafe/kitsunecafe.github.io" +description = "This website -- made with Roxy." + diff --git a/content/projects/legacy-kitsucafe.toml b/content/projects/legacy-kitsucafe.toml new file mode 100644 index 0000000..c673363 --- /dev/null +++ b/content/projects/legacy-kitsucafe.toml @@ -0,0 +1,6 @@ +title = "legacy kitsu.cafe" +created_date = "2021-12-06T00:00:00Z" +type = ["site"] +link = "https://github.com/kitsunecafe/legacy-kitsucafe" +description = "The old version of this site, made over a weekend with Gatsby." + diff --git a/content/projects/openpacker.toml b/content/projects/openpacker.toml new file mode 100644 index 0000000..4b3fe25 --- /dev/null +++ b/content/projects/openpacker.toml @@ -0,0 +1,6 @@ +title = "Open Packer" +created_date = "2022-08-11T00:00:00Z" +type = ["utility"] +link = "https://kitsu.cafe/open-packer/" +description = "Fork of Free Texture Packer." + diff --git a/content/projects/roxy.toml b/content/projects/roxy.toml new file mode 100644 index 0000000..b595601 --- /dev/null +++ b/content/projects/roxy.toml @@ -0,0 +1,6 @@ +title = "Roxy" +created_date = "2023-08-11T00:00:00Z" +type = ["utility"] +link = "https://fem.mint.lgbt/kitsunecafe/roxy-cli" +description = "A very small static site generator made with Rust." + diff --git a/content/projects/t3js.toml b/content/projects/t3js.toml new file mode 100644 index 0000000..e78860e --- /dev/null +++ b/content/projects/t3js.toml @@ -0,0 +1,6 @@ +title = "t3.js" +created_date = "2021-12-13T00:00:00Z" +type = ["utility"] +link = "https://kitsu.cafe/t3.js/" +description = "Fork of Three.js editor with some additional features." + diff --git a/layouts/footer.html b/layouts/footer.html new file mode 100644 index 0000000..c52d550 --- /dev/null +++ b/layouts/footer.html @@ -0,0 +1,2 @@ +
© Kitsune Cafe {{ now() | date(format="%Y") }}
+ diff --git a/layouts/index.html b/layouts/index.html new file mode 100644 index 0000000..6c5c45f --- /dev/null +++ b/layouts/index.html @@ -0,0 +1,22 @@ + + + + {% block head %} + + + + + + + {% block title %}Kitsune Cafe{% endblock title %} + + {% endblock head %} + + +
+ {% block body %} + {% endblock %} +
+ + + diff --git a/layouts/macros.html b/layouts/macros.html new file mode 100644 index 0000000..2f67eaf --- /dev/null +++ b/layouts/macros.html @@ -0,0 +1,14 @@ +{% macro message(text, reversed=true, classes="", avatar) %} +{% set flex = "row" %} +{% if reversed %} + {% set flex = "row-reverse" %} +{% endif %} +
+
+ +
+
+

{{ text }}

+
+
+{% endmacro message %} diff --git a/layouts/page.html b/layouts/page.html new file mode 100644 index 0000000..bb8e124 --- /dev/null +++ b/layouts/page.html @@ -0,0 +1,25 @@ +{% extends "index.html" %} + +{% block body %} +{{ super() }} +
+ +
+
+ {% block content %} + {% endblock %} +
+
+ {% include "footer.html" %} +
+{% endblock %} + diff --git a/layouts/post.html b/layouts/post.html new file mode 100644 index 0000000..d8058ef --- /dev/null +++ b/layouts/post.html @@ -0,0 +1,7 @@ +{% extends "page.html" %} + +{% block content %} + {% block article %} + {% endblock %} +{% endblock %} + diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..bc8e41c --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,263 @@ +:root { + box-sizing: border-box; + font-family: Verdana, Helvetica, Tahoma, sans-serif; + + + --primary-color: #EA80FC; + --secondary-color: #AA4FF6; + --tertiary-color: #7461AD; + --quaternary-color: #794A7F; + + --primary-complement-color: #92fc80; + --secondary-complement-color: #9aad61; + --tertiary-complement-color: #9aad61; + --quaternary-complement-color: #507f4a; + + --primary-triad-1-color: #FCEA80; + --secondary-triad-1-color: #F6AA4F; + --tertiary-triad-1-color: #AD7461; + --quaternary-triad-1-color: #7f794a; + + --primary-triad-2-color: #80FCEA; + --secondary-triad-2-color: #4FF6AA; + --tertiary-triad-2-color: #61AD74; + --quaternary-triad-2-color: #4a7f79; + + --black: #181A1B; + --dark-grey: #232323; + --grey: #A9A9A9; + --light-grey: #D3D3D3; + --white: #EEEEEE; + + --dark-violet: #391648; + --light-violet: #ecc0ff; + + --foreground-color: var(--white); + --background-color: var(--black); + --secondary-background-color: var(--dark-violet); + + --border-color: var(--secondary-color); + --header-color: var(--secondary-triad-1-color); + + --link-color: var(--secondary-triad-2-color); + --link-hover-color: var(--primary-triad-2-color); + --link-active-color: var(--tertiary-triad-2-color); + --link-visited-color: var(--quaternary-triad-2-color); + + --list-marker-color: var(--secondary-triad-1-color); +} + +@media (prefers-color-scheme: light) { + :root { + --foreground-color: var(--black); + --background-color: var(--white); + --secondary-background-color: var(--light-violet); + + --border-color: var(--quaternary-color); + --header-color: var(--quaternary-color); + + --link-color: var(--quaternary-complement-color); + } +} + +* { + box-sizing: inherit; +} + +html, body { + width: 100%; +} + +body { + --b: var(--background-color); + --f: var(--secondary-background-color); + + margin: 0; + background-color: var(--b); + color: var(--foreground-color); + + background: radial-gradient(circle, transparent 20%, var(--b) 20%, var(--b) 80%, transparent 80%, transparent) 0% 0% / 48px 48px, radial-gradient(circle, transparent 20%, var(--b) 20%, var(--b) 80%, transparent 80%, transparent) 24px 24px / 48px 48px, linear-gradient(var(--f) 1px, transparent 1px) 0px -0.5px / 24px 24px, linear-gradient(90deg, var(--f) 1px, var(--b) 1px) -0.5px 0px / 24px 24px var(--b); + background-size: 48px 48px, 48px 48px, 24px 24px, 24px 24px; + background-color: var(--b); + +} + +@media only screen and (min-width: 740px) { + body { + margin: initial; + } +} + +h1, h2, h3, h4, h5, h6 { + font-family: Arial, Georgia, Helvetica, sans-serif; + font-variant: small-caps; + margin: 0; +} + +a { color: var(--link-color); } +a:hover { color: var(--link-hover-color); } +a:active { color: var(--link-active-color); } + +li { + list-style: none; + position: relative; + + margin-left: 8px; + padding-left: 4px; +} + +li::before { + content: '★'; + position: absolute; + left: -12px; + color: var(--list-marker-color); +} + +pre { + padding: 0.5rem; + overflow: auto; +} + +nav.main-nav { + display: flex; + flex-wrap: wrap; + min-width: 140px; + justify-content: center; +} + +nav li::before { + content: ''; +} + +@media only screen and (min-width: 740px) { + nav.main-nav { + flex-direction: column; + justify-content: initial; + } + + nav.main-nav li::before { + content: '★'; + } +} + +.container { + display: flex; + flex-direction: column; + + justify-content: center; + + width: 100%; + max-width: 800px; +} + +@media only screen and (min-width: 740px) { + .container { + flex-direction: row-reverse; + margin-top: 1rem; + } +} + +.full-width { + width: 100%; +} + +.full-width.container { + max-width: initial; +} + +.column { + display: flex; + flex-direction: column; +} + +.row { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.row-reverse { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; +} + +.column .align-center, .row .align-center { + align-items: center; +} + +.column .justify-center, .row .justify-center { + justify-content: center; +} + +.column .space-between, .row .space-between { + justify-content: space-between; +} + +.card { + background-color: var(--background-color); + border: 1px solid var(--quaternary-color); + margin: 0.75rem; + padding: 1rem; + border-radius: 4px; +} + +.card.has-moon { + position: relative; +} + +.card.has-moon::before { + content: ''; + position: absolute; + top: 18px; + right: 18px; + height: 24px; + width: 24px; + box-shadow: -4px 4px 0 1px var(--secondary-triad-1-color); + border-radius: 50%; +} + +.card-header { + color: var(--header-color); +} + +.card-image { + margin: 0.25rem; + margin-right: 0.75rem; + float: left; + height: 100px; + width: 100px; +} + +.card-content { + display: inline-block; + padding-left: 1rem; +} + +.card-image img { + transition: transform .25s ease; + image-rendering: smooth; +} + +.card-image img:hover { + transform: scale(1.1) translateY(-5px); +} + +.hide-overflow { + overflow: hidden; +} + +.muted { + color: var(--grey); +} + +.center-text { + text-align: center; +} + +.grow { + flex-grow: 1; +} + + + diff --git a/static/css/message.css b/static/css/message.css new file mode 100644 index 0000000..55606ac --- /dev/null +++ b/static/css/message.css @@ -0,0 +1,75 @@ +:root { + --message-accent-border-color: var(--primary-triad-1-color); +} + +@media (prefers-color-scheme: light) { + :root { + --message-accent-border-color: var(--tertiary-triad-1-color); + } +} + +.bubble { + position: relative; + outline: 1px solid var(--border-color); + border-radius: 4px; +} + +.tail:before { + content: ''; + background-color: var(--background-color); + display: block; + position: absolute; + width: 10px; + height: 10px; + border-width: 1px; + border-color: var(--border-color); + transform: rotate(45deg); + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); +} + +.bubble.accent, .bubble.accent .tail.left, .bubble.accent.tail.left::before { + outline-color: var(--message-accent-border-color); + border-color: var(--message-accent-border-color); +} + +.tail.right:before { + right: -6px; + top: 18px; + border-style: solid solid none none; +} + +.tail.left:before { + left: -6px; + top: 18px; + border-style: none none solid solid; +} + +.message { + flex-wrap: nowrap; + margin-bottom: 1rem; +} + +.message .avatar { + align-self: start; +} + +.message .avatar img { + border-radius: 50%; + height: 64px; + width: 64px; +} + +.message .content { + flex-grow: 1; + padding: 0.25rem 1rem 0.25rem 1rem; +} + +.message .content.left { + margin-left: 1rem; +} + +.message .content.right { + margin-right: 1rem; +} + diff --git a/static/img/anon.webp b/static/img/anon.webp new file mode 100644 index 0000000..87bb19b Binary files /dev/null and b/static/img/anon.webp differ diff --git a/static/img/kitsucafe-88x31.png b/static/img/kitsucafe-88x31.png new file mode 100644 index 0000000..116195b Binary files /dev/null and b/static/img/kitsucafe-88x31.png differ diff --git a/static/img/kitsucafe-88x31.webp b/static/img/kitsucafe-88x31.webp new file mode 100644 index 0000000..81139c4 Binary files /dev/null and b/static/img/kitsucafe-88x31.webp differ diff --git a/static/img/pfp.webp b/static/img/pfp.webp new file mode 100644 index 0000000..71333c1 Binary files /dev/null and b/static/img/pfp.webp differ diff --git a/static/img/pfp64.webp b/static/img/pfp64.webp new file mode 100644 index 0000000..24fb5bf Binary files /dev/null and b/static/img/pfp64.webp differ diff --git a/watch b/watch new file mode 100755 index 0000000..ec0e8f7 --- /dev/null +++ b/watch @@ -0,0 +1,12 @@ +#!/usr/bin/env sh + +kill_server() { + kill "$server" +} + +roxy_cli content dist +inotifywait -mre create,delete,modify content layouts | while read dirname events basename; +do + roxy_cli content dist +done +