▶ Watch the gameplay demo · 1 minute
A first-person inspection game in the spirit of Papers, Please, built solo from scratch in Unreal Engine 5.7. All gameplay systems are C++; Blueprints are used only for content wiring, FX and widget layout.
No template, no tutorial scaffolding. Every system below was designed as a deliberate architectural choice, and the engine-behaviour claims behind those choices were verified against Unreal's own source rather than assumed.
You work a night toll booth. A vehicle arrives, the driver hands you their papers, and you approve or reject. Each shift comes with its own rule set, announced on the orders board in the booth. Approving a violation costs you money; catching one pays. Run out of money and your shift is over.
Some drivers are not drivers. Letting one of those through ends the run immediately.
Rules are a polymorphic UObject hierarchy (UTollRule), not an enum plus a
parameter struct. Each concrete rule owns its own editable parameter and answers two
questions:
virtual bool IsViolatedBy(const FDriverData& Driver) const; // does this driver break it?
virtual FText GetRadioBriefing() const; // what does the board announce?Both live in the same class on purpose: it is structurally impossible for the board to announce a rule the economy does not enforce. Adding a rule means adding a class — no existing code is touched.
Marked EditInlineNew, DefaultToInstanced, so a designer picks a rule class from a
dropdown in the editor and fills its parameter inline.
ATollManager (the game's model) publishes events and has no idea who listens:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMoneyChanged, int32, NewMoney);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameOver, ETollGameOverReason, Reason);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDayStarted, int32, DayNumber);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnShiftEnded);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCampaignCompleted);The manager previously reached for GetPlayerController(0) and cast to the view in
four places — a dependency pointing the wrong way that would also have updated only
player zero in multiplayer. The proof the refactor landed: TollManager.cpp no
longer includes TollPlayerController.h at all.
One delegate per distinct event, never a merged "something changed" — a merged event
forces the subscriber to re-derive information the publisher already had. Parameters
carry only what a subscriber cannot act without; FOnShiftEnded takes none, because
the call itself is the entire message.
A shift's briefing, rules and drivers only ever change together, so they live in one struct rather than three parallel arrays that could drift out of sync:
USTRUCT(BlueprintType)
struct FTollDayData
{
UPROPERTY(EditAnywhere) FText ShiftIntroBriefing;
UPROPERTY(EditAnywhere, Instanced) TArray<TObjectPtr<UTollRule>> Rules;
UPROPERTY(EditAnywhere) TArray<FDriverData> Drivers;
};StartDay() is the single definition of "a day begins", so the first day and the
fourth are structurally identical.
Every player-facing string goes through NSLOCTEXT + FText::Format, so translators
can reorder words instead of receiving pre-assembled sentences. Numbers are formatted
by meaning, not by habit: money uses the locale's grouping ($ 1,024), while years
and day numbers are identifiers and suppress it (Day 1024, never Day 1,024).
The orders board is a real object in the world (UWidgetComponent in
EWidgetSpace::World), not a HUD overlay — the player has to look at it. It pulls
its own text from the manager and reprints on every new day; the player controller is
not in that path at all.
| Engine | Unreal Engine 5.7 |
| Language | C++ (all systems), Blueprint (content wiring, FX, widget layout) |
| IDE | JetBrains Rider |
| VCS | Git + Git LFS |
Conventions follow the Epic C++ Coding Standard: TObjectPtr<> for every reflected
object reference, b prefix on bools, project-wide Toll class prefix. Source is
ASCII-only; all player-facing text lives in the localization tables.
- Clone the repository (Git LFS required for binary assets).
- Right-click
NightToll.uproject→ Generate Visual Studio project files. - Build the
NightTollEditor | Development Editor | Win64target. - Open
NightToll.uproject.
The level dresses itself from free Fab asset packs, which are not committed — they are several gigabytes of source textures and would put every clone behind a multi-gigabyte download. The code, the Blueprints and the level are all here; the level will simply open with those meshes missing.
To restore it, add these to the project from your Fab library: Modular Container Houses (Nando Studio), Construction Site VOL.1 (Dekogon), Vehicle Variety Pack (Switchboard Studios), Apartment Tech Props (Dekogon).
All gameplay systems are written from scratch; the packs supply meshes and materials only.
The full loop runs end to end: multi-day campaign, per-day rule sets, document inspection, economy, two failure endings and a completion ending.
Development is tracked per phase in PROJE_DURUMU.md (Turkish), which records each
system, the decision behind it and the alternatives that were rejected. The commit
history follows the same phases.