For HarmonyOS game development, this article presents a four-layer split across Store, System, Engine, and UI to solve state bloat, logic coupling, and scalability issues. Core takeaway: Store holds state only, System implements rules only, and Engine handles orchestration. Keywords: HarmonyOS, game architecture, state management.
Technical Specification Snapshot
| Parameter | Description |
|---|---|
| Target Platform | HarmonyOS / HarmonyOS Mini Games |
| Primary Language | ArkTS / TypeScript |
| Architectural Style | State-driven + domain-based separation + orchestration layer |
| Core Principles / Patterns | Single responsibility, stateless System, centralized orchestration |
| Star Count | Not provided in the source |
| Core Dependencies | ArkUI, Store state container, System business modules |
AI Visual Insight: This image serves as a structural overview of the article’s theme. It highlights the layered relationship between game state, rule systems, and UI presentation, making it a good entry-point diagram for architectural decomposition rather than a concrete API screenshot.
AI Visual Insight: This animated image works more like a dynamic process demo. It communicates the idea that state changes drive visual updates, helping developers understand that the UI is not the center of game logic, but a visual projection of state.
Basic layering breaks down quickly as HarmonyOS games become more complex
Many projects start with only three layers: Store, System, and UI. At first, that feels simple enough. But once you add skills, levels, drops, AI, and cross-device synchronization, the problems appear immediately: Store turns into a giant object, Systems start calling each other, and the UI is forced to patch in business logic.
The real issue is not that ArkUI is unsuitable for complex games. The issue is that many teams still use a page-oriented mindset to build what is actually a systems engineering problem. Once a game reaches medium complexity, it is no longer a collection of pages. It is an evolving state system.
You should define the most important boundary first
Store is the data layer and should only hold current state. System is the behavior layer and should only modify state according to rules. UI should only subscribe to state and render it. Any cross-layer overreach eventually becomes maintenance cost.
// GameStore.ts
export class GameStore {
playerHp: number = 100 // Player HP
enemyHp: number = 100 // Enemy HP
level: number = 1 // Current level
exp: number = 0 // Current experience
gold: number = 0 // Current gold
}
This code turns Store into a pure state container and prevents it from carrying business rules.
Store loses control when state and rules live in the same class
Early in development, many teams put attack, levelUp, dropItem, and enemyAI directly into GameStore. That speeds up short-term delivery, but it destroys long-term maintainability because one class now owns state, flow control, decision logic, and side effects at the same time.
In essence, this approach compresses the entire game into a God Object. The usual outcome is a triple failure: hard to test, hard to extend, and hard to collaborate on. Once one feature changes, a chain of unrelated logic can break with it.
Proper decomposition starts with single responsibility
Store should manage state and basic reads and writes, but it should not carry complex rules. System should execute behaviors, modify state, and drive flow, but it should not persist long-lived state itself. This is the first principle of decomposition.
// Anti-pattern: putting logic into Store
class BadGameStore {
enemyHp: number = 100
attackEnemy() {
this.enemyHp -= 10 // The business rule is directly coupled to the state object
}
}
This code shows the anti-pattern: once a state object starts handling rules, it almost always continues to bloat.
Strengthening System is how you keep Store lightweight
Once you move logic into System, state and rules naturally decouple. System does not own data. It only operates on the Store passed into it, which makes it reusable, composable, and testable. It behaves more like a set of pure business capability modules.
Combat rules should live in an independent System
// system/BattleSystem.ts
import { GameStore } from '../store/GameStore'
export class BattleSystem {
attack(store: GameStore) {
store.enemyHp -= 10 // Apply the attack rule
if (store.enemyHp <= 0) {
store.enemyHp = 0 // Keep state within valid bounds
}
}
takeDamage(store: GameStore) {
store.playerHp -= 5 // Apply the damage rule
if (store.playerHp <= 0) {
store.playerHp = 0 // Clamp HP at zero
}
}
}
This code extracts combat rules from the state container and turns them into an independent, stateless business module.
System should be split by domain, not by file growth
When BattleSystem gets larger, do not keep stuffing more logic into it. Split it by domain into BattleSystem, LevelSystem, DropSystem, and AISystem. The criterion is not “there is too much code.” The criterion is “the responsibilities are now different.”
The value of domain-based decomposition is that each System has a clear boundary: combat owns damage and resolution, leveling owns experience and level progression, drops own probability and rewards, and AI owns decision-making and behavior selection.
A single directory reveals whether your architecture is healthy
// system/
// ├── BattleSystem.ts // Combat rules
// ├── LevelSystem.ts // Leveling rules
// ├── DropSystem.ts // Drop rules
// └── AISystem.ts // AI decision rules
This code shows the structural boundaries created by domain modeling, which makes team collaboration and module expansion much easier.
Leveling rules should exist as an independent unit
// system/LevelSystem.ts
import { GameStore } from '../store/GameStore'
export class LevelSystem {
addExp(store: GameStore, exp: number) {
store.exp += exp // Increase experience
if (store.exp >= 100) {
store.level += 1 // Level up when the threshold is reached
store.exp = 0 // This example uses a reset-to-zero strategy
}
}
}
This code encapsulates progression logic as an independent rule unit so it does not mix with combat logic.
Adding an orchestration layer is what turns multiple Systems into a stable flow
As the number of Systems grows, a new question appears: who decides execution order? If the UI directly calls battle, level, and drop in sequence, the presentation layer becomes the business orchestration center again.
That is why you need an Engine as an orchestration layer. It does not hold core business state, and it does not implement concrete rules. Instead, it drives the overall flow, manages ordering, and isolates the UI from Systems.
GameEngine is the only recommended coordinator between Systems
import { GameStore } from '../store/GameStore'
import { BattleSystem } from './system/BattleSystem'
import { LevelSystem } from './system/LevelSystem'
class GameEngine {
battle = new BattleSystem()
level = new LevelSystem()
update(store: GameStore) {
this.battle.takeDamage(store) // Process damage first
this.level.addExp(store, 1) // Then process experience gain
}
}
const store = new GameStore()
const engine = new GameEngine()
setInterval(() => {
engine.update(store) // Centralized orchestration for the game loop
}, 16)
This code uses Engine to connect multiple Systems and establish a stable game update loop.
Preventing Systems from calling each other is a key step in controlling coupling
If BattleSystem directly calls LevelSystem, and LevelSystem then calls DropSystem, the dependency graph quickly spirals out of control. In the end, no module can be tested independently, and one change can affect the entire system.
The correct approach is to let Engine orchestrate all collaboration. Each System should care only about the inputs and outputs of its own domain and should not actively depend on other Systems. That is how the architecture stays flat, clear, and predictable.
The final recommended four-layer structure
// UI -> Engine -> System -> Store
// UI: presentation layer, only responds to state changes
// Engine: orchestration layer, manages execution order
// System: rules layer, encapsulates domain logic
// Store: state layer, holds the single source of truth
This code summarizes an extensible HarmonyOS game architecture with the smallest possible model.
This decomposition directly solves four core problems
First, complexity becomes manageable. Each System can evolve independently instead of forcing all logic into one class. Second, the architecture is extensible. You can continue adding quests, inventory, skills, and achievements without breaking the backbone.
Third, the design becomes testable. To test BattleSystem, you only need to pass in a mockStore, without depending on the UI. Fourth, the architecture is naturally friendly to multiple clients. Because Store is the single source of truth and System is a pure logic layer, multiple UI front ends can work around the same state model.
The most common developer mistakes all come from responsibility leakage
Mistake one: Store fills up with logic and becomes a God class. Mistake two: System holds state, for example by caching data long-term through this.store, which creates lifecycle confusion. Mistake three: UI adds business patches and starts owning flow decisions.
Once these signs appear, your architectural boundaries have already weakened. The fix is not to keep adding helper functions. The fix is to return to clear responsibility boundaries: state, rules, orchestration, and presentation should each stay in their proper place.
What you are really building is a state transformation system, not a collection of pages
At the advanced stage, you need a mindset shift: game development is not about “building a few feature pages.” It is about defining the mapping between inputs, rules, state changes, and rendered results. Clicks, AI decisions, and timers are all input sources. System performs the transformations, Store records the results, and UI renders them automatically.
That is also the key to sustainable evolution in HarmonyOS game architecture. As long as the layering stays clear, the system can remain stable as business complexity grows, instead of collapsing under temporary logic.
FAQ
1. Why should Store not contain methods like attack or levelUp?
Because that turns the state object into a business object, coupling state, rules, and flow together. It may save time in the short term, but it destroys testability and extensibility over time.
2. Why should System not hold a reference to Store?
Once System owns state, it introduces lifecycle and synchronization issues. This becomes especially hard to control in multi-page or multi-client scenarios. Stateless Systems are easier to reuse, compose, and test.
3. Will GameEngine become a new “big class”?
No, as long as Engine only handles orchestration and does not carry concrete business rules. Its job is to coordinate execution order, not to replace BattleSystem or LevelSystem in implementing details.
Core summary: This article restructures the core architecture of HarmonyOS games as complexity grows: let Store hold state only, let System focus on rule execution, and use GameEngine for centralized orchestration. This solves state bloat, system coupling, and UI-level business logic leakage.