Game Jam · GitHub Game Off · 2023
Survive & Fry
Overview
Survive & Fry is a cooking game built for GitHub's Game Off 2023, a month-long jam running November 1st to December 1st with over 600 entries. The theme was SCALE and players assembled food orders to cure zombies rather than escape them, with a literal food scaling step baked into the preparation pipeline as our theme interpretation.
The Preparation Pipeline
The preparation loop runs through four steps before a sandwich can be served. Chop a vegetable at the chopping desk, combine the chopped ingredient with the bread, add the antidote, and scale the portion to match the current order before submitting it.
Each step is enforced through the item combination system rather than a locked sequence. The player can attempt steps in any order as the bread accepts any valid ingredient and only rejects duplicates. Correctness is judged once, at serve time, when ValidateServe checks the finished sandwich against the current order. This creates a loop where reading the current task before preparing is the natural behaviour rather than something the game has to force.
Correct serves add 5 seconds to the session timer. Wrong serves deduct 10. That asymmetry is where most of the pressure in the game comes from.
The Ingredient System
Ingredients are tracked on the bread using a TSet of FName values rather than individual boolean flags. Each item in the world carries an IngredientName property on the base AItem class. When a chopped item is combined with the bread, the system casts to AItem, reads the name, and either adds it to the owned ingredient set or handles it as an antidote.
This means no per-type casts are needed anywhere in the combination logic. Adding a new ingredient requires a new DataTable row and six lines of code, with no changes to the interaction or validation logic.
The ingredient mesh map, which links ingredient names to their corresponding visual mesh components on the bread, is populated in BeginPlay rather than the constructor. UPROPERTY TMaps are serialized into Blueprint subclasses, so anything added in the constructor risks being overridden by cached Blueprint data when new ingredients are added later. BeginPlay rebuilds the map fresh every time, which avoids that entirely.
The antidote is handled separately because it has unique visual behaviour, a particle effect and a sound, that does not fit the generic ingredient pipeline.
Order Generation
Each order is generated by PickNewOrder, which selects a random row from the IngredientDataTable and a random scale value between 1 and 3. The DataTable row defines the ingredient's internal name and its display name for the HUD.
Adding Onion as a New Ingredient
To make this concrete, adding onion as a new ingredient works like this.
In Bread.h, declare the new mesh component (2 lines):
UPROPERTY(VisibleAnywhere, meta = (AllowPrivateAccess = true))
class UStaticMeshComponent* OnionVegetableMesh;
In Bread.cpp, create and attach it in the constructor, and register it in IngredientMeshMap in BeginPlay, across four lines.
// Constructor
OnionVegetableMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Onion"));
OnionVegetableMesh->SetupAttachment(RootSceneComponent);
OnionVegetableMesh->SetMobility(EComponentMobility::Movable);
// BeginPlay
IngredientMeshMap.Add(TEXT("Onion"), OnionVegetableMesh);
Then create a whole onion Blueprint inheriting from AVegetable with CanBeChopped = true, and ChoppedItemReference pointing to a chopped onion Blueprint inheriting from AChoppedVegetable, with IngredientName "Onion". Place the whole onion on a respawning item desk in the level and connect the references. Add a new row to the DataTable with IngredientName "Onion" and DisplayName "Onion Sandwich".
That is everything. No changes to ServingDesk, CombineItems, or ValidateServe anywhere. The full process from code change to a working in-game order takes around five minutes.
Serve Validation
ValidateServe handles both success and failure outcomes in a single function. It checks three conditions, whether the bread contains the required ingredient, whether the antidote has been added, and whether the current scale matches the order. Common cleanup runs after both outcomes, clearing the desk, resetting the scale, picking a new order, and destroying the bread.
Development Under Jam Constraints
A month is not a lot of time. The architecture decisions that made the project manageable were ones that kept iteration contained. When something needed adjusting, the change was localised and did not ripple into unrelated systems. That kind of clean separation matters a lot when iteration cycles need to be fast.
The game was well received by the community, and praised for polish, feel, and clean visuals at launch.
Post-Jam Refactor
The jam version shipped with all of the game rules living in the player controller. The countdown timer, the zombie counters, and the win and lose checks all sat next to the HUD code. After the jam I moved the rules into a dedicated GameMode class, which is where they were expected to live. The controller now owns only what makes sense, which is the HUD, the camera, the pause menu, the end screens, and the serving desk reports each attempt to the GameMode instead of reaching into the controller.
The save system got the same treatment. It started as a Blueprint based save feature in game instance, and I rebuilt it in C++ with a Blueprint child which holds asset references. Widgets cast to the C++ base class rather than the Blueprint, which keeps hard asset references out of the UI for simplicity.
The refactor also changed what the high score measures. The first version scored a run as starting time minus remaining time, which was directly affected by successful and failed serves. Twenty successful serves injected a hundred bonus seconds into every winning run, and the numbers were skewed. The fix was to track elapsed match time separately from the countdown. TimeLeft is considered the amount of time remaining to serve the orders, elapsed time became the actual unbiased score. HUD elements were updated to display the actual elapsed time at the top right corner to avoid confusion.
The playable build linked above is the original jam submission. The refactor lives in the repository.
What I Learned
Jam constraints force you to make decisions you would normally postpone. Scope creep is not possible when the deadline is real, which means every feature has to justify its existence immediately. That pressure produced better scoping decisions than I would have made with unlimited time. It is a discipline I try to carry into personal projects now even without a deadline forcing it.