Code the Forge: Building a Crafting System From Scratch

Game developer coding a forge crafting system on a workstation

Code the Forge: Building a Crafting System From Scratch

Code the forge: building a crafting system from scratch

A player walks up to a glowing anvil, opens a menu, sees a list of recipes, drags a few iron ingots into a slot, and watches a progress bar fill while sparks fly. The whole interaction looks like a single game feature, but in practice it is a small chain of subsystems: item definitions, recipe data, input validation, state transitions, UI feedback, persistence, and balancing. When a studio decides to code the forge it is rarely because the team cannot imagine a crafting screen. It is because the team has to decide which engine pattern, which data structure, and which validation rules will keep the system maintainable from prototype through live operations.

This article is written for game developers, technical designers, and solo programmers who need a working mental model before they write the first line of code. It covers how a forge interacts with the inventory, how recipes should be modeled as data rather than hard-coded branches, where to draw the line between client and server authority in multiplayer, and how to balance production so a forge is useful without breaking the economy. The approach stays engine-agnostic where it can, so the same patterns apply in Unity, Unreal, Godot, or a custom engine, with engine-specific callouts where the API surface actually changes the design.

What a forge actually does inside a game

A forge is a station that converts inputs into outputs under rules. The inputs are usually items from the player’s inventory, sometimes combined with a tool durability cost, a fuel cost, a skill level, or a time cost. The output is typically an item, a stack of items, or a modified version of an existing item. The rules cover recipe eligibility, station state, world conditions, and progression gates such as a minimum smithing level or a faction reputation.

Stripped of art, audio, and animation, every forge implements the same five steps in roughly the same order:

  • Look up the recipes the player is allowed to see at this station.
  • Filter those recipes against the player’s current inventory and progression state.
  • Accept a player confirmation and consume the required inputs.
  • Run the craft, either instantly or over a duration, and produce the outputs.
  • Persist the result, update the UI, and surface a feedback signal such as a sound, a particle, or a notification.

Each of those steps can be implemented in many ways, and the choice has consequences. A hard-coded switch statement in a forge component is fast to write but expensive to extend. A data-driven recipe table is slower to design but cheap to maintain once the content team takes it over. A server-authoritative queue is safe for competitive play but adds latency to a feature that players expect to feel responsive.

Before any code is written, the most useful question is not “how do I open a crafting menu” but “who owns the truth about what the player has, what the player can craft, and what the player just crafted.” That question decides whether the forge is a client decoration or a backend-tracked system, and once it is answered late in development the answer is painful to change.

Choosing the right scope for your forge

Forges range from a single NPC vendor that exchanges tokens for gear to a multi-stage metallurgy system with alloys, heat treatment, quenching fluids, and failure rolls. The amount of code a project needs should match the design goal, not the ambition of the genre. A small survival game can ship a credible forge with a few hundred lines of logic. A large RPG with thousands of recipes, mod support, and live service tuning needs a different architecture entirely, with separate services for content, validation, and persistence.

When a simple inline forge is enough

If the forge is a single station with fewer than about thirty recipes, all of which are known at design time, an inline implementation is usually enough. A struct on the forge actor holds a list of recipe IDs, a method checks the player’s inventory for the required tags and quantities, and a confirmation method removes inputs and adds outputs. This pattern fits neatly into a single script and is easy to read, which matters when a small team has to debug a craft that consumes the wrong item in the middle of a playtest.

Inline forges also work well for prototypes, game jams, and vertical slices where the goal is to validate the player’s emotional response to crafting rather than to test long-term balance. The trade-off is that every new recipe requires touching code, the content team cannot add items without a programmer, and balancing has to happen in the source tree, which means a recompile and a rebuild for every tweak.

When to move to a data-driven forge

Once the number of recipes, the number of stations, or the number of branching rules grows beyond what one person can hold in their head, the forge needs to become data-driven. Recipes live in a table, a JSON file, a ScriptableObject library, or a remote configuration service. The forge actor reads its recipe list from data, and a single generic craft method handles every station in the game from a blacksmith to a cookfire to a repair bench.

This shift has three practical benefits. First, designers can ship a new forge in a content patch without a code deploy. Second, balance changes are localized to a data file, which means faster iteration and a clearer review trail in version control. Third, the same forge code can power every crafting station in the game, which keeps the codebase smaller and the testing surface narrower.

Designing the recipe data model

The data model is the spine of the forge. Get it wrong and every later system, from UI to save games to analytics, pays the cost. A clean recipe model has at least four pieces of information: identity, inputs, outputs, and gating. Each piece should be a stable, machine-readable value rather than a display string, because the save format, the analytics pipeline, and the network protocol will all key off these values.

Field Purpose Example Notes for implementation
Recipe ID Stable identifier for save data and analytics recipe.iron_sword Use a string or namespaced enum, never a display name, because localization changes the visible text
Station tag Which forge can run this recipe station.anvil Drives UI filtering and prevents a sword recipe from appearing at a cookfire
Inputs List of required item entries 2x iron_ingot, 1x leather_strip Each entry should store item ID, quantity, and an optional consume flag for catalysts that survive the craft
Outputs What the player receives 1x iron_sword Allow multiple outputs for branching crafts and byproducts such as slag or offcuts
Skill requirement Progression gate smithing_level >= 3 Keep this in data so designers can tune progression without code changes
Craft time Duration or instant flag 4.0 seconds Drives UI progress bars, audio length, and server tick rate
Failure roll Optional chance to consume inputs without output 0.1 on missing skill Keep failure deterministic on the server and visible to the client
Unlocks Optional prerequisite recipes or quest flags recipe.tongs, quest.first_heat Prevents the UI from showing recipes the player has not earned yet

Two fields deserve extra attention. The first is the input consume flag, because it lets a single recipe support catalysts, tools, and fuel without inventing a second recipe type. A smithing hammer can be flagged as non-consumed, a piece of coal as consumed, and a smithing mold as consumed only on failure, all inside the same input list. The second is the failure roll, because it is the easiest place to introduce hidden randomness that breaks player trust. If failure exists at all, it should be predictable, communicated in the tooltip, and reversible by skill, not a silent dice roll that punishes a player who thought they had a guaranteed craft.

Inputs as a list, not a switch

A common beginner mistake is to write a craft method that hard-codes each recipe as its own branch with its own input check. This works for two recipes, becomes unmaintainable at twenty, and locks the content team out of the system. The fix is to model inputs as a list of entries and let a generic loop do the work:

  • Read each required entry from the recipe definition in declaration order.
  • Look up the player’s inventory slot or slots that hold that item ID.
  • Verify the combined stack has at least the required quantity.
  • Mark the matched slots for consumption only after every other input has passed validation.

This pattern scales to any number of inputs, supports partial stacks across multiple slots, and makes it easy to add new entries such as fuel or a coolant without changing the algorithm. The same loop also makes it easy to surface a useful error: if a recipe needs three iron ingots and the player has two, the UI can show “iron ingot: 2 of 3” rather than a generic “cannot craft.”

Connecting the forge to the inventory system

The forge is meaningless without an inventory. The interface between them is the most common source of bugs in crafting code, so it deserves a clear contract. Treat the inventory as the single source of truth for what the player holds, and treat the forge as a client of that interface. The forge should ask the inventory questions and react to answers, never reach into the inventory’s internal data structures to do its own bookkeeping.

Inventory operation What the forge should ask Failure mode to handle
Can craft Does the inventory contain every required input with sufficient quantity? Partial stacks across slots, missing catalysts, items locked in a quest slot
Reserve inputs Mark the matched slots as reserved so other systems do not consume them mid-craft Race conditions during long crafts, especially in multiplayer or with quick-stack
Consume inputs Subtract the reserved quantities from the reserved slots Player dropping or trading the item while the craft is queued
Add outputs Insert the crafted items into the inventory and stack where possible Full inventory, weight limit, stack overflow on legacy data
Cancel reservation Release any reserved inputs if the craft is cancelled or rejected Player logout, disconnect, menu close, or server rejection

A useful discipline is to give the inventory a small set of methods that the forge can call, and to refuse to let the forge reach into the inventory’s internal data structures. This keeps the inventory replaceable, which matters when a prototype inventory is later swapped for a grid-based or weight-based system, and it stops craft logic from leaking into every system that needs to know what the player carries. The same discipline applies in reverse: the inventory should not know which station produced an item, only that an item was added.

UI, feedback, and the player experience of crafting

The UI is where the forge becomes a feeling rather than a function. Players should be able to see at a glance which recipes are available, which inputs they are missing, and what they will receive. A craft screen that hides required quantities behind a tooltip is technically correct but practically frustrating, because the player has to compare two numbers in their head every time they hover a recipe. Good forge UI borrows from the inventory screen and from comparable crafting systems in the player’s mental library rather than inventing a new paradigm.

At minimum, the craft screen should show:

  • The list of recipes available at the current station, filtered by skill and unlock state.
  • A selected recipe panel with a clear list of inputs, each annotated with a count the player has and a count the recipe needs, and a visual marker for missing inputs.
  • An output preview with a count, a quality or rarity hint if the system supports it, and a tooltip that explains the resulting item.
  • A craft button that is enabled only when every input is present, the station is in the right state, and the player meets the progression gate, and that shows a clear reason when it is disabled.
  • A progress indicator for timed crafts, with a clear cancel option and a refund preview if the design allows cancellation.

Feedback is not optional. A forge that silently consumes iron and silently produces a sword feels like a bug even when it works. A short craft sound, a particle burst, a camera shake on a critical success, and a toast notification on a failure are small touches that turn a working system into a satisfying one. Accessibility matters in the same pass: a screen reader hint that names the recipe and the result, a colorblind-safe icon for low-quantity warnings, and a vibration cue on supported controllers can be added in a single UI pass and reach players who would otherwise be excluded.

Networking and authority: who decides the craft succeeded

Single-player games can keep the entire forge on the client. Multiplayer games cannot, because a client that decides for itself which items it just produced can cheat. The standard solution is server authority, in which the client sends a craft request to the server, the server validates inputs and progression against the authoritative inventory, and the server returns the result. The client then updates its local copy of the inventory to match.

This raises two design questions. The first is responsiveness. A player who clicks “Craft” and waits half a second for the server to respond will feel lag, especially in a fast-paced survival game where a half-second pause is enough to die. The standard mitigation is optimistic UI: the client pretends the craft succeeded for the duration of the round trip, then reconciles if the server rejects it. The second question is concurrency. Two players using the same forge at once can race to consume the same inputs, and a queue without reservations can drop one player’s craft silently.

Pattern Best for Cost Failure mode to watch for
Client-side instant craft, server reconciliation Casual co-op, low-value crafts Cheat risk on valuable outputs Duplication exploits when the client and server disagree about inventory state
Server-authoritative queue with reservation Competitive multiplayer, MMO crafting Extra round trip and queue UI Player disconnect mid-craft leaves inputs reserved until timeout
Server-side determinism on a fixed tick Lockstep simulations, deterministic netcode Forces fixed-step crafting and synchronized clocks Drift between clients if the craft step is not a multiple of the tick
Hybrid: client validates, server re-validates Most modern multiplayer games Two implementations of the same rules Rules drift between client and server when one is updated and the other is not

Pick the pattern that matches the trust model. A casual co-op sandbox can accept the duplication risk in exchange for snappier UX. A competitive survival game cannot. The important point is to make the choice explicit, document it next to the forge code, and ensure that the design team understands which classes of exploit are out of scope. A short design note in the repository, listing the cheat vectors the team has decided not to defend against, saves hours of confused code review later.

Balancing the forge without breaking the economy

A forge that is too generous collapses the game’s progression. A forge that is too punishing drives players to other systems or, worse, to external wikis and spreadsheets where they look up the exact input combination for the one recipe that is worth their time. Balance is not a one-time task; it is a loop of design, measurement, and tuning, and the loop is only as fast as the data the team can see.

Three metrics are worth tracking from day one:

  • Craft attempts per player hour, to see which recipes are popular and which are dead.
  • Conversion rate from recipe selection to successful craft, to catch recipes that look desirable but are rarely completed, usually because inputs are too rare or the skill gate is too high.
  • Median inputs left over, to see whether players are hoarding resources because no recipe uses them, which is a signal that the recipe table has gaps.

These metrics do not require a full analytics platform. A simple event log in development, a counters struct on the forge actor, and a debug overlay in the build are enough for early tuning. Once the game is live, the same events can feed a dashboard, and a balance pass becomes a config change rather than a patch. Balance also lives in the data. Skill requirements, craft times, and failure chances are all data fields, which means balance changes can ship as configuration updates rather than recompiles. A common mistake is to hide balance constants inside the source code where only a programmer can change them. Pulling them into a data file or a remote config cuts the iteration loop from days to minutes, which is the difference between a team that tunes the game and a team that argues about it.

Failure handling and edge cases

The difference between a prototype forge and a shippable forge is mostly how the edge cases are handled. A useful checklist covers the cases that will eventually be reported by QA or by players in the first week of release:

  • What happens if the player opens the forge, walks away, and the inventory is modified by another system such as auto-pickup or a companion.
  • What happens if the player has the required items in multiple stacks rather than one consolidated stack.
  • What happens if the player cancels a long craft halfway through and expects a partial refund.
  • What happens if the save game is loaded while a craft is in progress, both on the client and on the server.
  • What happens if the recipe list changes between sessions and a saved craft references a removed recipe.
  • What happens if the server rejects a craft the client has already shown as in progress, especially after a long round trip.
  • What happens if the player’s input items are moved by another system while the reservation is open, for example a quicksort that swaps stacks during the craft.

Each of these has a sensible default. The player walks away and the forge closes the menu, releasing any reservations. The forge prefers the largest stack first to keep the inventory tidy, then fills from smaller stacks. A cancelled craft refunds inputs up to a defined percentage, which is communicated in the tooltip. A save file either prevents in-progress crafts on save or resumes them deterministically on load, with a clear marker in the UI. A removed recipe logs a telemetry event and drops the reference, so a content patch can never silently brick a save. A rejected craft rolls the UI back with a clear explanation, and any reserved inputs are returned to the inventory before the error toast appears. Building this checklist early costs a few hours. Building it after launch, when each edge case is a player complaint on the forum, costs weeks.

Testing the forge the way QA actually tests it

Crafting systems fail in the seams between systems, which is why a forge test plan should not stop at “does the recipe work.” A useful forge test plan covers three layers, and a serious project will run all three before every release.

The first layer is unit tests on the data and the validation logic. Given a recipe and a synthetic inventory, does the forge correctly report eligibility, consume the right slots, and produce the right outputs? These tests are cheap, run in seconds, and catch most regressions when a new recipe is added or an existing one is rebalanced. A test that takes a recipe, a synthetic inventory with a known state, and asserts the resulting inventory is the kind of test that pays for itself the first time a designer renames a field.

The second layer is integration tests on the inventory and the persistence layer. After a craft, is the saved inventory correct? After loading a save, are the in-progress crafts either resumed or cleaned up? These tests catch the bugs that only appear across sessions, which is where most live-service crafting bugs actually live. A test that crafts an item, saves the game, loads the game, and asserts the item is still in the right slot is more valuable than ten unit tests on the craft method.

The third layer is playtesting, in which humans try to break the system. Common exploits to test for include crafting during a server handoff, using a macro to spam the craft button, queueing more crafts than the inventory supports, exploiting a cancellation refund to multiply inputs, and crafting with a stack that is being moved by another system. A playtest report that lists only “crafting feels slow” is a missed opportunity. A good report ties the feel to a measurable cause, which is what a developer needs to fix the system. A report that says “crafting feels slow because the success animation is 1.2 seconds and the queue does not start the next craft until the animation finishes” is actionable; a report that says “crafting feels slow” is not.

A practical implementation outline

The following outline is engine-agnostic and is meant as a starting point rather than a finished implementation. It assumes a data-driven recipe model, a server-authoritative craft request in multiplayer, and a single forge UI component. Each step has a clear deliverable and a clear review gate, which is what turns a wishlist into a roadmap.

  1. Define an item registry that maps stable item IDs to display names, icons, stack sizes, and tags.
  2. Define a recipe table that lists every recipe with its station tag, inputs, outputs, skill requirement, and craft time.
  3. Implement an inventory service with a small public interface: can craft, reserve, consume, add, and cancel reservation.
  4. Implement a forge actor that reads its station tag, looks up the recipes, and exposes a craft request method.
  5. Add a craft validation method that checks inputs, skill, and station state before sending a request.
  6. Wire the validation method to a client UI that shows the recipe list, the input status, and the craft button.
  7. Add a server handler for the craft request that re-validates, consumes, and replies with the result.
  8. Add reconciliation on the client so that optimistic UI rolls back if the server rejects the request.
  9. Add telemetry events for craft attempts, success, failure, and abandonment.
  10. Add unit, integration, and playtest coverage for the edge cases listed in the previous section.

Step one through four can ship in a vertical slice and are enough to validate the core loop. Step five through eight are the production-grade path and are what the live game will actually run on. Step nine and ten are what turn the forge from a feature into a system the team can tune after launch, and they are usually the steps that get cut when a project runs out of time, which is exactly when they are most needed.

Frequently asked questions

How do I decide whether the forge is client-side or server-authoritative?

Start with the trust model. If the game is single-player or a casual co-op sandbox where cheating is tolerated, a client-side forge is faster to build and feels more responsive. If the game is competitive, has tradeable crafted items, or has a real economy, the forge must be server-authoritative so that no client can decide for itself which items it just produced. The choice should be made before the first line of code is written, because switching later means rewriting the craft validation, the inventory interface, and the save format, which is a much larger project than picking the right model on day one.

Should recipes be hard-coded in the engine or stored as data?

Store them as data once the number of recipes or the rate of recipe changes exceeds what one programmer can keep in their head. Hard-coded recipes are fine for a small prototype, but they push the content team into a code deploy for every balance change, which slows the team down and creates a bottleneck. A data-driven recipe table lets designers ship new content and tuning updates without touching the engine, and it keeps the forge code generic so the same logic can power every station in the game from a single shared codebase.

How do I keep the forge UI from going stale after balance changes?

Bind every visible value to the live recipe data rather than to a cached copy held on the UI component. The UI should read the current input list, the current required quantities, and the current craft time from the recipe definition on every open. This means a balance change is reflected in the UI the next time the menu is opened, with no code change. If the game supports remote configuration, the same binding means a live tuning update can shift a recipe’s output count without a patch, which is exactly the kind of fast iteration a live service needs.

What is the right way to handle fuel and catalysts in a recipe?

Model them as inputs with a consume flag set to false. The forge still validates that the player has the required quantity, but it does not subtract the item on a successful craft. This single design supports tools that survive crafting, fuel that is consumed per craft, and rare catalysts that are returned on failure, without inventing a parallel data model for non-consumed inputs. The flag is one boolean per input entry, and it is the cleanest way to keep the recipe table small and the craft loop uniform.

How do I prevent a player from spamming the craft button to dupe items?

Lock the craft button on the client as soon as the request is sent, and require the server to confirm the result before the button is enabled again. On the server, ignore duplicate requests for the same recipe and player within a short window, and reject any request whose claimed inputs do not match the player’s authoritative inventory. A playtest that includes automated spam clicks, ideally with a recorded timeline, is the most reliable way to catch dupe paths before launch, and it tends to surface race conditions that unit tests miss.

What should I log to balance the forge after launch?

At minimum, log a craft attempt event with the recipe ID, a success or failure outcome, the player’s skill level, and a timestamp. From those events you can compute craft attempts per player hour, the conversion rate from selection to success, and the distribution of skill levels for each recipe. These three numbers tell you which recipes are popular, which are frustrating, and which are so cheap that the rest of the economy has to compensate. A fourth useful field is the input snapshot, which lets you replay a specific failed craft in the editor later.

How do I localize a forge without breaking save data?

Localize display strings, not identifiers. Recipe IDs, item IDs, and station tags should remain stable strings or enums, and the localized name and description should live in a separate string table keyed by those identifiers. The UI binds to the identifier and looks up the localized text at render time, so a player who switches language mid-session sees the correct name without a save migration, and a translator who edits the string table can never accidentally rename a recipe ID and break a thousand saved games.

What is the smallest useful prototype for a forge?

A single station actor, a list of three to five recipes defined inline, an inventory stub with add and remove methods, and a UI panel that shows the recipe list and a craft button. This takes a few hours to build and is enough to test whether the core loop of “collect, craft, receive” feels good. Once the loop is validated, replace the inline recipes with a data table, replace the inventory stub with the real inventory system, and add the validation, persistence, and feedback layers described in this guide. The order matters: validate the feel before investing in the architecture.

Where can I learn more about the historical idea of a forge as a workshop?

The cultural and historical background of a forge as a metalworking workshop is covered in the Wikipedia article on forge, which is useful context for designers who want to ground their in-game forge in real metallurgy without turning the feature into a simulation. The article is a starting point, not a design spec, and any in-game mechanic should still be balanced for the player’s experience rather than for historical accuracy, because a game that follows history too closely tends to be as tedious as the work it depicts.

How does this forge pattern relate to other crafting systems like cooking or alchemy?

The same pattern covers them. A cooking station is a forge with a station tag of “campfire” and a recipe table of food entries. An alchemy lab is a forge with a station tag of “alchemy_table” and recipes that include consumable catalysts and byproducts. By keeping the forge logic generic and parameterizing it with station tag, recipe table, and progression skill, the same code ships the entire crafting layer of the game, which keeps testing, balancing, and UI consistent across every system the player encounters and reduces the surface area for bugs.

Internal links

Leave a Reply

Your email address will not be published. Required fields are marked *