The hardest part of building game systems isn't the initial implementation. It's keeping them maintainable as the project grows. Chain Crisis is a long-term project, so we've codified a set of design principles that guide how we structure code.
These principles aren't theoretical. They're lessons learned from systems that broke down under complexity, from refactoring sessions that took weeks, and from watching codebases become unmaintainable. Here's what seems to work.
Separation of Concerns
Each class or component should have one clear job. This sounds obvious, but it's easy to blur the lines, especially when you're trying to get something working quickly.
In our AI system, BTService_EvaluateTactics only evaluates rules and sets blackboard values. It doesn't execute actions or modify characters directly. That's the job of behavior tree tasks. This separation means rules can be evaluated without touching execution logic, and vice versa.
The same principle applies everywhere: services evaluate, tasks execute, components store data, characters own configuration. When responsibilities are clear, changes stay localized.
Data Belongs on the Entity It Affects
Configuration should live where it's used, not in some central manager. If a character has a combat aiming speed, that property belongs on the character class, not the AI controller.
This seems like a small thing, but it matters. When properties are on the character, each character blueprint can have different values. Designers can tune per-character without code changes. Inheritance works naturally. The character becomes the single source of truth for its own configuration.
Too many systems have configuration living in controllers or managers, making it hard to understand what affects what. Keeping data with the entity it configures makes the system self-documenting.
Data-Driven Over Hard-Coded
When you need different behavior for different action types, add a CSV column, don't write an if/else chain. This is one of the most impactful principles.
if (Action == UseWeapon)
RotationSpeed = 30.0f;
else if (Action == Follow)
RotationSpeed = 10.0f;
// ... grows forever// CSV: ...,Action,...,RotationSpeed
// UseWeapon,...,30.0
// Follow,...,10.0
float Speed = Rule.RotationSpeed; // Read from dataDesigners can tune values without code changes. Adding new variants is just adding a CSV row. Version control diffs are readable. The separation between "what" (the data) and "how" (the code that interprets it) stays clear.
Explicit Over Implicit
Make behavior visible. If a function sets focus as a side effect, that should be explicit in the behavior tree, not hidden inside a service.
Hidden side effects are debugging nightmares. When something goes wrong, you can't see what's happening. Explicit behavior trees act as documentation—you can read the tree and understand the flow.
Functions should do what their name says. If it's called SetBlackboardValues, it should only set blackboard values. If it also sets focus and changes rotation speed, those should be separate, visible steps.
Graceful Fallbacks
Systems should handle edge cases smoothly. In our AI, we use a priority hierarchy with fallback behaviors:
Combat Actions (80-180) ← Active combat
↓ (cooldowns/range)
CombatIdle (50) ← Fallback: maintain awareness
↓ (no enemies)
Follow (40) ← Fallback: follow leader
↓ (no leader)
Patrol (10) ← Fallback: idle behaviorThis creates natural transitions. When combat actions aren't available, the AI doesn't suddenly switch to following—it maintains combat awareness with CombatIdle. When enemies are gone, it transitions to following. When there's no leader, it falls back to patrolling.
The system never gets stuck. There's always a valid next state. This makes debugging easier because states are explicit, and it creates a better player experience because transitions feel natural.
Composition Over Inheritance
Prefer components over deep inheritance hierarchies. A character has a TacticComponent, a CombatComponent, a FactionComponent—features are mixed and matched through composition.
This gives flexibility. Not every character needs every component. Testing is easier because you can mock components. Dependencies are clearer. You avoid the inheritance diamond problem and deep hierarchies that become hard to understand.
Inheritance still has its place—for fundamental "is-a" relationships and core engine integration. But for optional features and cross-cutting concerns, components are the better choice.
Test Incrementally
When adding a new feature, implement it for one case first, test thoroughly, then expand. Don't try to do everything at once.
When we added the CombatIdle rule, we started with one character (Hani). We tested: does she maintain combat awareness? Does the transition to Follow work? Once we validated the behavior, we applied it to other characters and adjusted per-character values.
This catches problems early, when they're easier to fix. It also gives you confidence that the system works before you scale it up.
Document Decisions, Not Just Code
Comments should explain why, not what. The code already shows what it does. What's missing is the reasoning behind the decision.
Maintaining design documents that capture the "why" behind architectural choices. When someone asks "why did we do it this way?", the answer should be documented. This is especially important for non-obvious decisions and constraints.
Code comments should link to documentation when relevant. They should explain assumptions and constraints. They should help future developers (including yourself) understand the context.
Why These Principles Matter
These aren't academic exercises. They're practical decisions that make long-term development sustainable. When systems follow these principles:
- Changes stay localized—modifying one system doesn't break others
- New team members can understand the codebase faster
- Designers can iterate without constant code changes
- Debugging is easier because behavior is explicit and traceable
- The system remains flexible as requirements evolve
Building for the long term means making decisions that compound over time. These principles are how we structure code so it remains maintainable as Chain Crisis grows.