Game dev logs, software notes, and personal writing.
This journal holds two sides of my writing: build logs from software and game development, and more personal notes on technology, animal care, culture, politics, religion, reviews, and everyday life.
Game development, software systems, and build notes.
Devlogs, systems notes, and practical writing from projects that are still being built.
Current read
What performance started to mean to me in application code.
One thing I have been thinking about more seriously is how performance changes the way I judge architecture. Early on, it is easy to focus only on whether a system works, whether the abstraction feels clean, or whether the design reads well. Those things still matter to me. But the longer I work on real applications and game systems, the more I feel that performance is not some isolated concern waiting at the end. It is part of the design from the beginning.
A lot of that became clearer to me when I thought more carefully about polymorphism, virtual methods, and overriding in C#. On paper, polymorphism feels elegant. One interface, many implementations, and a system that can grow without rewriting its core logic. That elegance is real. It keeps code flexible, makes systems easier to extend, and usually leads to better architecture. But once performance enters the conversation, elegance stops being purely theoretical.
A virtual call is not the same as a direct call. The runtime has to resolve which implementation to execute based on the actual object type, not just the reference type. In most applications, that cost is small enough that it barely matters. But "barely matters" is not the same thing as "never matters." Once the same operation happens millions of times, even small overhead becomes real.
That is one of the most useful performance lessons I keep returning to: performance problems are often made of small costs repeated at scale. A single virtual call is not scary. A single allocation is not scary. A single lookup, cast, event dispatch, or extra layer of indirection is not scary. But once those things live inside a hot path, they stop being abstract language features and start becoming part of the frame time, the responsiveness, and the total cost of the system.
That does not mean polymorphism is bad. It means abstraction has a price, and part of professional engineering is knowing when that price is worth paying. In most application code, maintainability still wins. If polymorphism makes the design clearer, keeps the system easier to extend, and prevents conditional logic from spreading everywhere, then the tiny runtime cost is usually the right tradeoff. I would rather keep the architecture clean than flatten everything too early out of vague performance anxiety.
But I also think performance discipline means not hiding behind clean architecture when the usage pattern has clearly changed. If a method sits inside a UI event and gets called occasionally, I will think about readability first. If it sits in an AI loop, a stat recalculation path, an animation system, a pathfinding update, or some other part of the code that runs constantly, then I start thinking differently. That is where architecture and performance stop being separate conversations.
To me, that is the maturity point: understanding that good design is not only about flexibility, and optimization is not only about speed. Both are part of the same judgment. The systems I trust most are the ones that stay abstract where abstraction helps, and become more direct where cost starts to accumulate. A good engineer does not throw away polymorphism out of fear. A good engineer also does not keep every abstraction forever just because it looked elegant when the system was smaller.
Another thing performance taught me is that profiling matters more than assumption. It is easy to blame the wrong thing. A developer might see virtual methods and assume they are the problem, while the real cost is in allocations, string work, repeated lookups, poor caching, unnecessary synchronization, or data structures being used carelessly. The runtime is already optimized in many places. So if I care about performance, I need evidence, not instinct.
That is why I have become more skeptical of vague optimization advice. "Avoid virtual methods" by itself is not a useful rule. "Never use inheritance" is not a useful rule either. The real question is always where the cost is, how often it happens, and what I would be giving up if I optimized that part. Performance work becomes much healthier once it is tied to context instead of slogans.
I also think hot paths deserve a different kind of honesty. In performance-sensitive parts of an application, the cleanest architecture is not always the most layered one. Sometimes a flatter, more explicit, less abstract path is the correct solution. That does not mean the whole codebase should collapse into shortcuts. It just means performance-critical systems should be designed with their real execution frequency in mind, not only with architectural purity in mind.
For me, the best performance mindset is not "make everything fast." It is "know what can afford elegance, and know what cannot." That changes how I look at application code. I still care about extensibility, substitution, readability, and clean interfaces. But I also care more about where those abstractions live, how often they execute, and what they cost once the system is under real pressure.
The longer I work, the more I feel that performance is really a form of honesty. It forces the code to admit what it costs. And once I know that, I can make better decisions about where flexibility belongs and where directness matters more. Good application performance is not just about micro-optimizing instructions. It is about designing systems that remain understandable while respecting scale.
Current read
How to write professional Unity packages: lessons from building a modular game foundation.
The more seriously I think about Unity package architecture, the less I see a package as a folder full of useful scripts. A good package feels more like a promise. It promises that the code has a clear responsibility, that other systems can depend on it without getting trapped inside it, and that failures, save state, diagnostics, and future growth have all been considered before the system was declared finished.
That matters because large game foundations become fragile faster than they look. In the beginning, it feels efficient to let everything talk directly to everything else. Crafting reaches into inventory. Inventory reaches into economy. Characters directly initialize stats, abilities, animation, AI, equipment, targeting, and dialogue because they all seem connected anyway. That speed feels real for a while, but once the project grows, the coupling becomes expensive. Bugs spread wider, tests become harder to isolate, and every change starts touching systems that should have been independent.
So the first rule I keep coming back to is ownership. Every package should know what it owns and what it does not own. A crafting package can own recipes, crafting requests, previews, transaction plans, crafting jobs, and crafting diagnostics. It should not secretly become the inventory package or the economy package just because it needs both of them. A characters package can own definitions, archetypes, lifecycle state, runtime identities, and snapshots without pretending it also owns stats, AI, dialogue, equipment, or targeting. A conditions package can own definitions, condition sets, resolver registration, and evaluation results without hardcoding knowledge of quests, world state, achievements, or inventory internals. That boundary discipline is what separates a reusable package from a script collection that only survives in one project.
The second rule is that optional integrations should be bridges, not hard dependencies. This is one of the biggest lessons package work has taught me. If the core crafting package directly depends on economy, inventory, conditions, and stations, then it is no longer a clean foundation package. It becomes a dependency magnet. A stronger structure is to keep a clear core like com.taarn.foundation.crafting and move optional integrations into bridges such as com.taarn.foundation.crafting.economy, com.taarn.foundation.crafting.inventory, and com.taarn.foundation.crafting.conditions. The core defines the contracts. The bridges implement those contracts using other systems. That keeps the ecosystem installable, testable, and flexible instead of forcing every project to adopt one exact stack.
I also think a professional package should explain itself through structure. If I open a package and see only a pile of scripts, I trust it less immediately. I want to see a package.json that clearly names the package and its dependencies. I want Runtime/ for build-safe code, Editor/ for tooling, Tests/ for proof, Samples~/ for adoption, and Documentation~/ for intended use. I want assembly definition files that separate runtime, editor, and test assemblies on purpose. That structure is not cosmetic. It is how a package tells the next developer, including my future self, that the boundaries were designed consciously.
The public API matters just as much. A professional package should make the safe path obvious. Names like PreviewCraft, Craft, TryGetSnapshot, CopySnapshotsTo, CaptureSaveData, and RestoreSaveData already teach the caller something before the documentation is even opened. PreviewCraft should not mutate state. Craft should. RestoreSaveData should not wipe live state and hope for the best. If the API names are vague, the package starts teaching uncertainty instead of safety.
That is also why I care so much about structured result types. A serious package should not explain an important operation with only true or false. It should say whether something succeeded, failed because of an invalid ID, failed because ingredients were missing, failed because a bridge rejected the request, or failed because a save schema is unsupported. Those result objects are not just good for debugging. They improve UI, RuntimeDebug tools, tests, authoring validation, and the overall readability of the system. A package that explains why it failed is much easier to trust than one that forces everyone to reverse-engineer failure from side effects.
Consistent IDs are another part of professionalism that is easy to underestimate. Stable IDs are the shared language of the ecosystem. If one package accepts uppercase names, another silently trims spaces, and a third normalizes dots differently, the project becomes harder to reason about before any gameplay even begins. I would rather be strict early with IDs like character.actor.player, crafting.recipe.health_potion, and condition.worldstate.compare than discover at runtime that every system interpreted the same name differently.
Transaction-heavy packages also taught me that rollback must be scoped. If a crafting request reserves ingredients, reserves currency, consumes items, grants outputs, and one bridge fails near the end, rollback should undo only what that transaction changed. It should not restore a whole inventory snapshot or an old wallet snapshot and accidentally erase unrelated work from another system. The same discipline shows up in save and restore. A professional restore flow should validate first, build temporary state, reject unsupported schema versions, apply unknown-ID policies, run migrations if needed, and only commit if the whole restore succeeds. If restore fails, live state should remain intact.
Diagnostics, validators, and optional RuntimeDebug bridges are where a package stops being merely usable and starts becoming truly professional. Diagnostics tell me what is registered, what is active, what failed recently, and how often. Validators catch duplicate IDs, invalid references, missing outputs, bad authoring values, and unsupported combinations before Play Mode. RuntimeDebug commands such as list, inspect, diagnostics, crafting.preview, or economy.transfer make the system understandable while the game is running. I do not think of these as extras anymore. They are part of the package's contract with the developer.
The same is true for comments and tests. XML comments should teach ownership, mutation, bridge behavior, and failure guarantees rather than repeating the type name. Tests should prove more than the happy path. Invalid IDs, duplicate registrations, bridge failures, listener exceptions, rollback behavior, restore rejection, and future schema handling all deserve coverage. A package that only proves it works when everything goes right has not really proven much.
What I keep concluding is that professional package design is less about clever code than it is about consistency. Every package should feel familiar to use. Every package should validate itself the same way, expose diagnostics the same way, isolate dependencies the same way, and protect runtime state the same way. That is how an ecosystem becomes easy to learn. Not because every system is simple, but because every system follows the same rules. To me, that is the real standard: a good Unity package does not only solve a problem. It teaches the user how that problem should be solved cleanly.
Current read
Why packages started to matter to me in bigger Unity projects.
The bigger a Unity project gets, the more I stop thinking about scripts as isolated files and start thinking about boundaries. A package, to me, is really a self-contained feature module. Instead of letting every reusable system live in one giant project folder, I would rather move stable systems into their own spaces: com.taarn.foundation.stats, com.taarn.foundation.abilities, com.taarn.foundation.questing, com.taarn.foundation.saving, com.taarn.foundation.identity. That shift starts sounding serious only after a project grows enough to show why it matters.
At the simplest level, a Unity package is just a folder with a package.json. But that file matters because it tells Unity this is a real package, not a random script dump. It gives the system a name, a version, dependencies, and a place in the Package Manager. A name like com.taarn.foundation.achievements already says something important to me: this is author- or studio-scoped code, it belongs to the foundation layer, and its responsibility is achievements. That naming discipline helps turn a folder structure into an actual map of the architecture.
What makes packages important to me is not ceremony. It is the mess they prevent. Without real package boundaries, large projects slowly turn into a web where player logic knows inventory, inventory knows questing, questing knows UI, UI knows saving, saving knows stats, and eventually everything knows everything. That kind of project becomes harder to test, harder to reuse, harder to debug, and harder to change without collateral damage. Packages force clearer ownership. Stats should own numbers and modifiers. Abilities should own execution, cooldowns, charges, and costs. Questing should own objectives and progress. Saving should own persistence, not everyone else's internals.
I also care a lot about the internal structure of a package because structure tells people how to trust it. Runtime/ should contain the code that can actually ship in a build. Editor/ should hold setup tools, validators, inspectors, and debugger windows that exist only inside Unity. Tests/ should prove that the package is not guessing. Samples~/ should give a concrete usage path. Documentation~/ should explain the intended workflow. Once a package has those sections, it stops feeling like loose scripts and starts feeling like a professional system.
Assembly definition files matter for the same reason. I see .asmdef files less as bureaucracy and more as boundary enforcement. They separate runtime code from editor-only code, control dependencies, and stop UnityEditor logic from leaking into builds. That separation becomes especially important in larger ecosystems, because compilation and dependency direction should be deliberate. A low-level package like identity or saving should not quietly depend on a high-level shell or scene-specific system just because no one noticed the coupling early enough.
The most important design rule, for me, is still single responsibility. A package should have one main job. An abilities package should execute ability timing, rules, costs, and cooldowns. It should not also become the saving package, the inventory package, the VFX package, and the UI package because it happens to touch all of those things. The same principle applies everywhere. Once one package tries to own too many jobs, it stops being a reusable module and turns back into a disguised script pile.
The more I think about foundation-grade packages, the more I care about the pieces around the main service. A strong package does not just expose one class and call it done. It usually needs authored assets, result types, diagnostics, validation, save data, and tools that make the package inspectable under pressure. A bool is rarely enough when a system can fail for many reasons. An ability package, for example, may need to explain whether an action failed because of cooldown, missing context, invalid target, or cost. That is why result types matter. They turn hidden failure into usable information.
Diagnostics and validators matter just as much. Diagnostics keep a package from becoming a black box at runtime. Validators stop broken authored content from slipping into play mode and becoming someone else's debugging problem later. In practical terms, that means an achievements package should be able to catch duplicate IDs or bad signals before the game runs, and it should also be able to tell me what is currently registered, what unlocked recently, and what failed. That is the difference between code that technically works and a system that is safe to build on.
I also think save ownership is one of the cleanest tests of package maturity. Each package should know how to describe and restore its own state. Achievements should own achievement progress. Questing should own quest progress. Stats should own stat state. Effects should own active effect state. The saving package should not need to know everybody else's internals. It should store sections, while each bounded system contributes its own save data contract. That is cleaner architecture and much easier to evolve.
What I want from packages is cooperation without entanglement. A combat system should not need to know how achievements work internally. It should be able to report something like combat.enemy_defeated and let achievements, questing, progression, loot, and saving each react in their own way. The same thing applies to interactions, rewards, effects, and identity. That is where packages stop feeling like folders and start feeling like architecture: each system owns its job, exposes a clean contract, and participates in a larger flow without trying to own the whole event.
This matters even more because I am not thinking about one script collection anymore. I am thinking about a reusable foundation library for bigger games. AI, abilities, stats, questing, dialogue, interaction, inventory, saving, achievements, effects, progression, factions, loot, and identity all become more valuable when they can stand on their own and still cooperate cleanly. If those boundaries are weak, the ecosystem collapses into tangled code. If they are strong, the same package can survive across multiple projects and genres.
That is why packages have started to matter so much to me. A Unity package is not just a folder. In a serious project, it is a bounded system with a clear responsibility, a public surface, tests, validation, diagnostics, editor tooling, and defined integration points. That kind of structure does not make development slower. It is what keeps a large project from becoming impossible to improve.
Current read
Make the game easy to tweak, or it becomes hard to improve.
One lesson I keep returning to in game development is that a good system is not only one that works. A good system is one that can be adjusted without dragging the project back through code every time a number needs to move. That matters most once the game grows past the first prototype, because the values that feel convenient to hardcode early quickly turn into friction later.
At the start it feels harmless to write movement speed, damage, cooldowns, spawn rates, and camera values directly into scripts. It is fast because the number is right there. But as soon as balancing begins, that convenience disappears. Every small change means opening code, editing a constant, entering play mode again, testing again, and turning the programmer into the bottleneck for work that should really stay open to iteration.
Games get better through repeated adjustment. Movement almost never feels right on the first pass. Damage numbers shift after playtesting. Enemy aggression changes once pacing becomes clearer. Cooldowns feel different in real combat than they do in a spreadsheet. Those values are not fixed truths. They are design variables, which is why I keep thinking more seriously about systems that are easy to tweak.
In Unity that usually means exposing the right things through ScriptableObjects, inspector-driven configuration, data tables, or tooling that keeps game rules data-driven wherever practical. I would rather let a stat asset own player values, an enemy definition own combat numbers, and an ability asset define cooldowns, costs, cast time, targeting rules, and display data. Code should still define behavior, validation, and safety. But the values designers need to tune should not require source-code edits every time.
That distinction keeps getting more important to me: code should define behavior, while data should define variation. Weapons, enemies, abilities, rewards, prompts, and pacing values become much easier to test, rebalance, and reuse once the stable runtime logic is separated from the editable game data. That kind of separation does not happen by accident. It has to be designed into the architecture from the start.
The benefit is bigger than speed alone. Tweakable systems also expose assumptions instead of hiding them. They make systems easier to validate, easier to reuse, and easier to evolve without fear. For me, that is one of the clearest engineering rules in game development now: if the game is painful to tweak, it becomes much harder to improve. Good code is not only code that runs. It is code that gives the rest of the project room to keep getting better.
Current read
What design patterns started to mean to me as a game developer.
When I first came across design patterns, they felt like one of those topics every programmer is supposed to understand long before they actually feel real. The deeper I got into game development, though, the more practical they became. At some point I started seeing the same lesson again and again: patterns are not really about making code look impressive. They are about recognizing the shape of a problem and reaching for a structure that has already proven useful.
Game development keeps bringing back the same kinds of pressure. Input handling, character states, UI updates, scene transitions, save systems, enemy behavior, spawned objects, and the constant fight to keep systems connected without turning the whole project into a knot. That is where patterns started to click for me. The State pattern helps when something clearly needs to be in one mode at a time. Command becomes useful as soon as actions can come from a keyboard, controller, AI, or even a networked source. Observer feels natural when UI, audio, quests, and achievements all need to react to one event without being welded together.
The longer I keep building, the more other patterns reveal their value in the same way. Strategy helps when rules and behaviors need to change without rebuilding the whole system. Factory gives object creation a consistent gate when creation rules matter. Object Pool stops feeling like a clever optimization and starts feeling like basic survival once bullets, particles, and repeated effects are being created constantly. Memento becomes relevant the moment save systems need to capture runtime state cleanly, and the Component pattern keeps proving why composition is so powerful in Unity as long as that flexibility is matched with discipline.
Some patterns I treat with more caution. Singletons can be useful for truly global services, but I have also seen how quickly they spread invisible dependency through a project. Service Locator and Dependency Injection matter to me less as buzzwords and more as architectural choices: they decide how systems are found, shared, and kept under control as the game grows.
What helps me most now is not trying to memorize every pattern in isolation. It is noticing the pressure in the system early enough to ask what kind of structure would make that part easier to reason about. Too many modes pushes me toward State. Reusable or queueable actions make me think about Command. Systems reacting to each other usually point toward Observer. Swappable rules suggest Strategy. Creation, spawning, saving, and reusable runtime objects bring Factory, Pooling, Memento, and Components into view.
For me, the real skill is not being able to recite pattern names just to say I know them. It is learning to recognize the problem early enough to choose a solution that keeps the project clean, flexible, and readable under pressure. That is a big part of what keeps game development exciting for me. Every design decision shows up somewhere later, whether in performance, bugs, iteration speed, or how confidently I can build the next feature. Good game code is not just code that works today. It is code that still gives me room to build what comes next.
Opinions, field notes, and the more personal side.
This side is looser and more personal: opinions, observations, reviews, diary-like notes, and smaller pieces shaped by whatever has been on my mind.
Reading list
Current read
Stray animals, loud opinions, and the kind of decency I actually trust.
The internet makes opinions cheap. It is easy to sound morally serious in public, and much harder to stay decent in ordinary moments when nobody is watching closely.
One small test I keep returning to is how people treat stray animals. Not as performance, but as instinct. Whether they notice, pause, and make room for something vulnerable tells me more than polished statements usually do.
The kind of decency I trust is quiet, repetitive, and inconvenient. It shows up in habits, not slogans, and it survives privacy just as well as it survives applause.