top of page

Word Flow: System Design Breakdown

  • Writer: Harshita Shetye
    Harshita Shetye
  • 5 days ago
  • 11 min read

When I started building "Word Flow," I had three main goals for the code: keep it neat, make it easy to grow, and build it so that a non-programmer could help shape the game. I wanted to create a strong foundation that we could build on for years, not just a prototype that would fall over.


1. A Tidy Workshop of Managers


I structured the game's brain like a tidy workshop. Instead of one giant machine that does everything, I built a collection of specialized tools called Managers. The AudioManager's only job is to play sounds. The ScoreManager only handles the score. If there's a problem with the audio, I know exactly which tool to pick up and fix, without worrying about breaking the scoring system. This "one job" approach keeps everything clean, predictable, and easy to debug.


2. Digital Index Cards for Game Data


A core idea for this project was to separate the game's rules from its content. I treated all the item information, like a chair's name, cost, and picture, as data on little digital index cards called ScriptableObjects.


This is a game-changer because it means we can add a hundred new decorations to the game without ever writing a single new line of code. A designer could create a new "index card," fill in the blanks, and the game would instantly know how to use it. This makes balancing the game's economy and adding new content incredibly fast and flexible.


3. Making Unity Designer-Friendly


Finally, I believe a great system is one that anyone on the team can use. I spent time making the Unity editor itself a friendly tool for designers. I added helpful [Tooltips] to explain what different settings do and used [Range] sliders to prevent someone from accidentally setting the volume to a negative number. It’s like putting clear labels and safety guards on a machine. It empowers the whole team to make changes confidently and creatively, which is how great games are made.



Let's take a look at how the code is structured and designed.


Project Hierarchy Structure
Project Hierarchy Structure

Scripts Folder Structure
Scripts Folder Structure

Data Scripts


These scripts don't control any action in the game. Instead, they act as blueprints that define the "what". For example, what is a decoration? What is a sound? By keeping this data separate from the game's logic, the entire project becomes more flexible and easier to manage.


1) DecorationItem.cs


This script is the digital blueprint for every single item a player can buy in the shop.

Why a ScriptableObject? Instead of a regular game component, I chose to make this a ScriptableObject. A ScriptableObject is just the recipe to a cake - hundreds of different recipes (our items) can be created as assets in the project, without ever cluttering up the game scene. This keeps our data neatly organized and separate from the live game.


Key Design Choices


  • Stable ItemID: You'll notice there's a public itemName (like "Wooden Chair") and a private itemID (like chair_wood_01). This is crucial. A designer might want to change the display name later, but the game's code needs a stable, unique ID to rely on so that save files or purchases never break.

  • Read-Only Data: All the variables are exposed as "read-only" properties (Ex: public int Cost => cost;). This means other scripts can easily read how much an item costs, but they are not allowed to change it. This creates a safe, one-way flow of data and prevents messy bugs where one script could accidentally change the cost of an item for the whole game.

  • Designer-Friendly Fields: Using [TextArea] for the name gives a designer more room to type in the Inspector, making it a more pleasant tool to use. It's a small touch that shows you're thinking about your team's workflow.


2) Sound.cs


This simple script combines all the properties for a single sound effect into one neat package.


It is a [System.Serializable] Class. This script isn't attached to any game objects. It's just a custom data container. The [System.Serializable] tag is what tells Unity, "Hey, let my AudioManager see and edit this list of sounds in the Inspector." It turns a simple class into a powerful, reusable tool for designers.


Key Design Choices


  • Named Sounds: Each sound has a name string. This allows us to play sounds from code in a very readable way, like AudioManager.Instance.Play("ValidWord"). This is much safer and clearer than using numbers or directly linking audio files, which can easily break.

  • Creative Control with [Range]: I gave the sound designer direct control over each sound's volume and pitch with sliders. The [Range] attribute acts as a safety, preventing them from entering a value that doesn't make sense (like a volume of 220%). This empowers the audio designer to fine-tune the game's feel without needing a programmer.

  • Hiding Complexity with [HideInInspector]: The AudioManager needs to assign an AudioSource to this class to make it play, but the designer never needs to see this. Hiding it from the Inspector keeps the interface clean and prevents any confusion. It's about showing users only the controls they need.


Decoration Scripts


These scripts handle the logic for the decoration and shop systems. The main goal was to create a flow where each part of the system has one clear job and doesn't need to know too much about the others.


1) DecorationSlot.cs


This is the simplest script in the system, but it plays a crucial role. It is like a small, labeled signpost. Each pre-placed (and initially invisible) item in the room has this script on it. Its one job is to tell the RoomManager which decoration belongs in that specific spot.


Key Design Choice


The most important detail here is that the slot only stores the item's unique ItemID (like chair_wood_01), not the entire DecorationItem asset. This is a design choice for decoupling. The slot doesn't need to know the item's name, cost, or picture. By keeping it lightweight and focused, its purpose is clear, and it's less likely to break if we change other parts of the item's data.


2) RoomManager.cs


This is the brain of the decoration system. It manages the state of the entire room, deciding what is visible and what isn't. Its job is to check what the player owns and then "turn on" the matching decorations.


Key Design Choices


  • Automatic Discovery: The RoomManager doesn't rely on me manually dragging every single decoration slot into a list in the Inspector. Instead, when the game starts, it automatically scans the room and finds all the DecorationSlot signposts. This is a huge win for robustness. It means a designer can add, duplicate, or remove decorations in the scene, and the manager will just work without any extra setup. It prevents the simple human error of forgetting to hook something up.

  • Performance with a Dictionary: Instead of a simple list, the manager stores the slots in a Dictionary. A dictionary is like a perfectly organized filing cabinet - you can pull out the exact file you need instantly by its name (ItemID). This makes the UpdateRoomVisuals function incredibly fast and ensures the game will run smoothly, even if we decide to have a room with hundreds of decorations.


3) ShopItemController.cs


This script is the brain for every individual button in the shop. Its job is to display an item's info and announce to the rest of the game when a player tries to buy it.


Key Design Choice


The most important design pattern here is the use of C# events for decoupling. The ShopItemController is "dumb" in a smart way. It doesn't know about the ScoreManager or the PlayerInventory. When a player clicks the buy button, the controller doesn't command those systems directly. Instead, it just sends up an announcement called an event, that says, "Hey, a purchase was just attempted for this item!"


Other systems, like the ShopManager, are listening for this announcement. When they see it, they jump into action to handle the purchase logic. This is like a fire alarm: the alarm itself doesn't put out the fire, it just alerts the systems that can. This keeps the button completely self-contained and makes our overall code architecture much cleaner and easier to manage.


Gameplay Scripts


These scripts are the heart of the word-finding game. They control the board, the tiles, and how the player interacts with them.


1) CameraController.cs


This script has one simple but crucial job: to make sure the game board is perfectly framed for the player, no matter what device they're on.


Key Design Choices


  • Single Source of Information: The GridManager is the authority on the grid's size, so the CameraController should ask the GridManager for that information. This avoids having two different places to define the grid size, which would be a heaven for bugs. It's like having one master clock instead of two separate clocks that you hope stay in sync.

  • Controlled Execution: Instead of just running on Start(), the GridManager will tell the CameraController when to adjust itself. This ensures the grid is always generated before the camera tries to frame it, creating a reliable startup sequence.


2) GridManager.cs


This is the architect of the game board. It's responsible for creating the grid, spawning the tiles, and keeping track of every tile's location.


Key Design Choices


  • Singleton Pattern: Making the GridManager a singleton provides a stable, global access point. Any other system that needs to know about the grid (like the WordBuilder or CameraController) can easily find it without needing a messy direct reference in the Inspector.

  • Centralized State: The private Tile[,] grid; array is the definitive "single source of info" for the state of the board. When a tile is cleared, it's removed from this array. This ensures that the game's data is always consistent and prevents bugs where a tile might be visually gone but still exist in the game's logic.

  • Data-Driven Letter Frequency: The letterDistribution string is a simple way to control letter frequency, but we can do even better. By moving this data into its own ScriptableObject, a game designer could easily create different letter sets for different languages or difficulty modes without ever touching the GridManager's code.


3) Tile.cs


This script is the heart of each individual letter on the board. It's an interactive component that knows its own state and position.


Key Design Choices


  • Clear Initialization: The GridManager immediately calls the Setup() method, giving the Tile everything it needs to function: its letter and its coordinates. This pattern is called Dependency Injection, and it creates components that are reliable and predictable.

  • Component Responsibility: In a component-based architecture, each piece should manage itself. The Tile is responsible for its own visual state (changing its color when selected) and for detecting player input (OnMouseDown). It doesn't need an outside manager to tell it that it's being clicked.

  • Self-Reporting on Destruction: When a Tile is cleared, its final job is to tell the GridManager that its spot on the board is empty. By handling this itself before being destroyed, it ensures the game's central state (GridManager's array) is always accurate.


4) WordBuilder.cs


If the Tile is a single button, the WordBuilder is the "session manager" for the player's entire drag action. It wakes up when a drag starts, keeps track of the word being built, and directs the other game systems when the drag ends.


Key Design Choices


  • Orchestrator: The WordBuilder's main role is to orchestrate events. It doesn't know how to validate a word or how to add score; it delegates those tasks to the specialized managers (WordValidator, ScoreManager). This keeps its code focused on its core responsibility - managing the word selection process.

  • Enforcing Gameplay Rules: This is the perfect place to implement core game rules, like the adjacency check. The system prevents players from selecting letters that aren't touching, making the code the guardian of the game's rules.

  • Robust Delays with Coroutines: Instead of using Invoke("Refill", 0.1f), which uses a string that can easily break, we use a Coroutine. Coroutines handle actions over time.


5) WordValidator.cs


This script acts as the game's dictionary. It has one highly focused job: to answer the question "Is this a valid word?" as quickly and efficiently as possible.


Key Design Choices


  • Performance with a HashSet: This is the most important design choice in this script. Storing tens of thousands of words in a simple list would make checking a word very slow, as the game would have to look through every word one by one. A HashSet is a specialized data structure that works like a perfect index. It can tell you if a word exists almost instantly, no matter how large the dictionary gets. Choosing the right data structure for the job is critical for performance, and a HashSet is the perfect tool here.

  • Decoupled Data Source: Instead of hard-coding the file path Resources.Load("wordlist"), the refactored script uses a [SerializeField] reference to the text file. This decouples the code from the data. A designer can drag any dictionary file (perhaps for a different language or difficulty) into the Inspector slot without a programmer needing to change any code.

  • Static Access: The IsValidWord method is static, meaning it can be called from any other script in the game without needing a direct reference to the WordValidator object. This makes it a simple, globally accessible utility, so the WordBuilder can just ask WordValidator.IsValidWord("hello").


Manager Scripts


These are the engines of the game, operating in the background to handle core systems like audio, player data, and currency. The design focus here is on creating efficient, self-contained systems that communicate cleanly without getting tangled up in each other's business.


1) AudioManager.cs


This is the game's dedicated sound engineer. Its one job is to play the correct sound effect on command, with the settings defined by the designer.


Key Design Choices


  • Optimized for Performance: The public sounds array is great for designers to set up in the Inspector, but searching through it every time we play a sound is slow. The refactored code converts this array into a Dictionary one time at the start of the game. This is a critical performance optimization that allows the manager to find any sound by name instantly, no matter how many sounds are in the game.

  • Respecting the Data: The original script only played the audio clip, ignoring the custom volume and pitch we set up in the Sound class. The new Play method reads all the properties from the Sound object (volume, pitch, and looping) and applies them to the AudioSource. This makes the system's features fully functional and empowers a sound designer to have full control over the game's audio landscape.


2) PlayerInventory.cs


This script is the player's digital backpack. It simply keeps track of which decoration items the player has purchased.


Key Design Choices


  • Decoupled Communication with Events: The original script directly called the RoomManager when an item was added. This creates a tight coupling - the inventory system has to know that a room manager exists. The refactored design uses a C# event (OnInventoryUpdated). The inventory doesn't know or care who is listening. It just sends out a broadcast saying, "My contents have changed". Any other system (like the RoomManager) can listen to this message and react accordingly. This makes the systems modular and independent.

  • Optimized for Queries: The inventory maintains both a List of the full item data and a HashSet of just the item IDs. The HashSet provides an almost instant way to check if the player owns a specific item, which is a much faster operation than searching through the List.


3) ScoreManager.cs


This is the game's bank. It securely manages the player's currency and nothing else.


Key Design Choices


  • Single Responsibility: The ScoreManager is not responsible for updating the UI text. A system that manages data should not be directly coupled to a system that displays it.

  • Decoupled UI with Events: Just like the inventory, the ScoreManager uses an event (OnScoreChanged). When the score changes, it simply announces the new value. It's the UIManager's job to listen for this event and update the ScoreText accordingly.


UI Scripts


These are the bridge between the player and the game's underlying systems. The design here focuses on creating a clean separation between state management (what screen you're on) and event handling (reacting to player actions).


1) UIManager.cs


This script is the master controller for the game's UI. It doesn't manage any single part of the game's logic, but rather the overall flow and state of the user interface. Its job is to direct which panels and buttons are visible at any given time.


Key Design Choices


  • Centralized State Control: By having one UIManager that handles switching between "Play Mode" and "Decorate Mode," we create a single, reliable place for managing the UI's state. This prevents bugs where two different scripts might try to show or hide the same panel, leading to conflicts.

  • Decoupled from Game Data: It updates the score display by listening for the OnScoreChanged event from the ScoreManager. The ScoreManager's only job is to manage a number.


2) ShopManager.cs


This script is dedicated only to the functionality of the shop. It builds the shop's display and handles the logic for purchasing items.


Key Design Choices


  • Coordinator: The ShopManager acts as a coordinator. The individual ShopItemController buttons don't know how to process a purchase - they just announce that a purchase was attempted. The ShopManager listens for these announcements and then coordinates with the other systems (ScoreManager, PlayerInventory) to make the purchase happen. This keeps the logic for a "purchase transaction" in one clean, central location.

  • Aware of Game State: When the shop is generated, the ShopManager first checks with the PlayerInventory to see which items the player already owns. This allows it to tell the buttons to display themselves as "already owned," ensuring the UI is always in sync with the player's actual data from the moment it appears.

Comments


bottom of page