Skip to content

Repository files navigation

Night Toll

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.


The loop

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.


Architecture highlights

Data-driven rule engine

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.

The model never knows the view

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.

Campaign data

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.

Localization is not a later problem

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).

World-space UI

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.


Tech

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.


Building

  1. Clone the repository (Git LFS required for binary assets).
  2. Right-click NightToll.uprojectGenerate Visual Studio project files.
  3. Build the NightTollEditor | Development Editor | Win64 target.
  4. Open NightToll.uproject.

A note on the environment art

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.


Status

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.

About

A "Papers, Please"-style inspection and deductive reasoning game built from scratch in Unreal Engine 5 using C++.

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages