OUGADEV / Projects / Ghost of Yotei
Unreal Engine 5.7 In Dev ← Back to Portfolio

Project Documentation

Ghost of Yōtei (UE5)

A third-person open-world action game built in Unreal Engine 5, inspired by Ghost of Tsushima / Yōtei. Blueprint-only, studio-pattern systems - designed as both a portfolio piece and a public tutorial series.

Unreal Engine 5.7 In Progress

Overview

Unreal Engine 5.7 In Development

A third-person open-world action game in UE5, inspired by Ghost of Tsushima / Ghost of Yōtei. All systems are Blueprint-only to keep the accompanying tutorial series accessible, while still following real production patterns used in shipped titles.

Project overview - replace with a screenshot of the current in-editor state

System Status

Character Movement
✓ Done
Health System
✓ Done
Interaction System
✓ Done
Quest Director
✓ Done
Quest UI
✓ Done
Dialogue System
✓ Done
Combat
- Planned
Enemy AI
- Planned
Ally Recruitment
- Planned

Devlog Video

Devlog video - paste your YouTube embed ID here

Project devlog - overall progress, architecture decisions, and lessons learned

Folder Structure

Content/ ├── _Core/ │ ├── GameMode/ BP_YoteiGameMode, BP_PlayerController │ ├── Player/ BP_PlayerCharacter │ └── GameInstance/ BP_YoteiGameInstance ├── Systems/ │ ├── Health/ BP_HealthComponent │ ├── Interaction/ BPI_Interactable, BP_InteractionComponent │ ├── Quest/ BP_QuestManager (subsystem), DT_Quests │ ├── Dialogue/ BP_DialogueManager, DT_Dialogue │ └── UI/ WBP_HealthBar, WBP_InteractionPrompt, │ WBP_PauseMenu, WBP_QuestTab └── Content/ ├── Maps/ L_Foundation └── Test/ BP_TestInteractable, BP_TestNPC

Tech Stack

Unreal Engine 5.7
LayerToolNotes
EngineUnreal Engine 5.7Blank template, no Starter Content
ScriptingBlueprints100% BP - no C++
InputEnhanced InputIMC_Default + individual IA_ assets
DataData Tables + JSON importOne file for all quests, one for dialogue rows
PersistenceGame Instance SubsystemBP_YoteiGameInstance owns all runtime state
EventsEvent DispatchersNo polling - everything publish / subscribe
CameraCineCameraActor subclassBP_DialogueCamera - repositioned per conversation
UIUMG WidgetsBound to dispatchers in Event Construct, never Tick

Architectural Principles

  • No polling. Every widget and system subscribes to an Event Dispatcher and only updates when something actually changes. Nothing reads state in Tick.
  • One source of truth. BP_YoteiGameInstance is the only place runtime state (active quests, completed quests, tracked quest) ever lives. Nothing caches a copy.
  • Data in tables, not assets. All quest and dialogue content lives in JSON-imported Data Tables. Adding a quest is adding a row - not creating a new Blueprint asset.
  • Additive, not ripping. Every system is designed so v2 features are additive - new nodes, new fields, new functions - nothing rewires what already works.

Foundation

Unreal Engine 5.7 Done

The bootstrapping layer every other system depends on. UE5 requires an explicit chain that Unity handles implicitly - this is the piece with no Unity equivalent.

Bootstrap chain: BP_YoteiGameMode → sets Default Pawn Class = BP_PlayerCharacter and Player Controller Class = BP_PlayerController. Assigned in Level World Settings. Without this, nothing spawns and no input binds.
BP_YoteiGameMode Class Defaults
BP_YoteiGameMode - Class Defaults with Pawn and Controller assigned
BP_PlayerCharacter in level
BP_PlayerCharacter spawned in L_Foundation

Enhanced Input Setup

All input goes through Enhanced Input. Legacy Input.GetAxis is not available in UE5.7.

AssetTypeAction
IMC_DefaultInput Mapping ContextContainer - added in BP_PlayerCharacter BeginPlay
IA_MoveInput Action (Vector2D)WASD movement
IA_LookInput Action (Vector2D)Mouse / stick camera
IA_JumpInput Action (Bool)Spacebar
IA_InteractInput Action (Bool)E key - triggers InteractionComponent
IA_ToggleMenuInput Action (Bool)Tab - wired in BP_PlayerController (not Character)

Health System

Unreal Engine 5.7 Done

BP_HealthComponent is an Actor Component attached to BP_PlayerCharacter. It owns all health state and broadcasts changes via Event Dispatchers - nothing polls it.

Variables

VariableTypeDefault
MaxHealthInteger100
CurrentHealthInteger= MaxHealth
bIsDeadBoolfalse

Event Dispatchers

  • OnDamaged(Amount: Int, CurrentHealth: Int) - fires on every TakeDamage call that lands
  • OnHealed(Amount: Int, CurrentHealth: Int) - fires on every Heal call
  • OnDeath - fires once when CurrentHealth reaches 0; bIsDead guard blocks all subsequent calls
WBP_HealthBar - progress bar updating in real time via OnDamaged dispatcher
Why dispatchers, not direct calls: WBP_HealthBar and the debug print logger both subscribe to OnDamaged independently. Neither knows the other exists - and neither knows about BP_HealthComponent's internals. Adding a third listener (damage popups, screen effects) costs zero changes to the component itself.

Interaction System

Unreal Engine 5.7 Done

A four-piece system mirroring Unity's IInteractable pattern. The component sphere-traces each tick, finds the closest interactable, and drives the prompt widget - all without the interactable knowing who's looking at it.

BlueprintRole
BPI_InteractableInterface - Interact, GetPromptText, GetPromptIcon. Any actor implementing this is automatically discovered.
WBP_InteractionPromptIcon + text overlay. ShowPrompt / HidePrompt are the only public functions - no state knowledge.
BP_InteractionComponentActor Component on the player. Sphere trace each tick, tracks closest interactable, calls ShowPrompt / HidePrompt on the widget.
BP_TestInteractablePlaceholder actor implementing BPI_Interactable for end-to-end testing.
Interaction prompt shown when player enters sphere trace range
BPI_Interactable interface
BPI_Interactable - the three interface functions
Typing rule: casting down to a specific type always needs an explicit Cast node, even when you know the object implements the interface. Blueprint can't guarantee type at compile time - this explains every type-mismatch fix in this system.

Quest Director

Unreal Engine 5.7 Done

A weighted, catch-up quest director inspired by Yōtei's dynamic quest surfacing. Underleveled players get Side / Exploration draws; overleveled players get pulled toward Main Quests. The draw logic never hard-returns null - it re-rolls into the next available category.

Data Structure - S_QuestData

FieldTypePurpose
TitleTextDisplay name in Quest Log
DescriptionTextJournal entry text
CategoryE_QuestCategoryMain / Side / Exploration / Bounty - drives weighted draw
RegionE_RegionWhich region the quest belongs to
QuestLineNameGroups quests under a named arc in the log
PrerequisitesArray<Name>Row names that must be completed first
NextQuestNameAuto-unlocks this row on completion (chain quests)

DrawAndUnlockQuest - Weighted Algorithm

Input: CurrentRegion (E_Region) Output: DrawnQuest (BP_QuestData ref) or None 1. ComputeCategoryWeights(Region) → returns weight floats for Main/Side/Exploration/Bounty → underleveled = high Side weight; overleveled = high Main weight 2. RemainingCategories = [Main, Side, Exploration, Bounty] 3. WHILE RemainingCategories not empty: a. Sum weights of remaining categories → TotalWeight b. RandomRoll = Random(0, TotalWeight) c. Walk categories, accumulate RunningSum → first category where RunningSum >= RandomRoll = PickedCategory d. Pool = GetAvailableQuests(PickedCategory, Region) e. IF Pool not empty: → Draw random quest from Pool → UnlockQuest(DrawnQuest) → Broadcast OnQuestUnlocked → RETURN DrawnQuest f. ELSE: → Remove PickedCategory from RemainingCategories → continue loop 4. IF all categories exhausted → RETURN None (trigger handles gracefully - idle chatter, empty board, etc.)

System Demo

Demo: DrawAndUnlockQuest running across different PlayerPowerLevel values - showing category shift

Trigger Systems

TriggerMechanicStatus
InterrogationLast enemy in squad → DrawAndUnlockQuestPlanned
Camp VisitorWeighted NPC at rest point → dialogue → drawPlanned
Settler Quest GiverNPC in settlement → interact → dialogue → drawPlanned
Bounty BoardBoard actor → interact → draw (no dialogue)Planned

Quest UI

Unreal Engine 5.7 Done

The Quest Log lives inside the Pause Menu and uses a three-box layout: questline sections (Box 1), quest entries for the selected section (Box 2), and a detail panel for the selected entry (Box 3).

Quest Log open in Pause Menu
WBP_QuestTab - three-box layout with section, entries, and detail panel

Widget Hierarchy

WBP_PauseMenu └── Widget Switcher (Tab = 1: Quests) └── WBP_QuestTab ├── Box 1 - ScrollBox of WBP_QuestSection (one per questline) │ └── WBP_QuestSection → bubbles OnSectionClicked ├── Box 2 - ScrollBox of WBP_QuestEntry (one per quest in section) │ └── WBP_QuestEntry → bubbles OnEntryClicked └── Box 3 - Single WBP_QuestDetail (persistent, updated in place)

Design Rule - Discovered Only

Only discovered questlines appear in the log - matching Ghost of Tsushima / Yōtei's own Tales list. Showing "The Kitsune" before you've encountered it would spoil that the storyline exists at all. The log builds from BP_YoteiGameInstance's ActiveQuests and CompletedQuests arrays, grouped by QuestLine field.
Quest entry selected
WBP_QuestEntry - selected state with highlight
Quest detail panel
WBP_QuestDetail - stage text, description, and track button

Dialogue System

Unreal Engine 5.7 Done

A Yōtei-style auto-advance system. Lines play through on a timer derived from the voice clip duration - no Continue button. The only player input available is hold-to-skip the entire conversation.

Confirmed from research: Ghost of Yōtei's dialogue is fully automated - lines play on their own, you cannot advance line-by-line, and the only available input is hold-to-skip the whole cutscene. V1 matches that exactly.
Dialogue box in game
WBP_DialogueBox - subtitle overlay with auto-advance timer running

Auto-Advance Timing

Duration = MAX(GetSoundDuration(VoiceClip), MinLineDuration) // MinLineDuration default: 2.5 s // Guarantees silent test rows hold long enough to read SetTimer(Duration) → fires AdvanceDialogue → reads NextLineID // If NextLineID is empty Name → EndDialogue // If NextLineID has a Choices array (v2) → show choice buttons instead

Two-Shot Camera

BP_DialogueCamera (a CineCameraActor subclass) is repositioned by ActivateDialogueCamera at the start of every conversation:

Midpoint = (SpeakerLocation + PlayerLocation) / 2 Offset = Midpoint + (Right Vector * SideDistance) + (Up * HeightOffset) Camera → SetActorLocation(Offset) Camera → LookAt(Midpoint)

System Demo

Demo: auto-advance dialogue running with two mannequins and the two-shot BP_DialogueCamera

Gotcha: WBP_DialogueBox must be created at level start in BeginPlay, not on first Interact. If it's created on Interact, its Bind Event won't be listening in time for that very first call - the first line silently fires and nothing shows.

All Widgets

Unreal Engine 5.7 Done

Every widget is UMG-based, event-driven, and never polls in Tick. All state binds in Event Construct via Event Dispatchers.

WidgetRoleBinds ToStatus
WBP_HealthBarScreen-space progress barOnDamaged, OnHealed, OnDeath
WBP_InteractionPromptIcon + text near crosshairShowPrompt / HidePrompt
WBP_DialogueBoxSubtitle panelOnLineChanged dispatcher
WBP_PauseMenuTab-based pause overlayIA_ToggleMenu via BP_PlayerController
WBP_QuestTabQuest Log inside Pause MenuOnQuestUnlocked, OnQuestCompleted
WBP_QuestSectionOne questline groupParent WBP_QuestTab
WBP_QuestEntrySingle quest rowParent WBP_QuestSection
WBP_QuestDetailRight-panel detail viewOnEntryClicked → SetQuestDetail

Tab Key Fix - BP_PlayerController

The pause menu toggle was moved from BP_PlayerCharacter to BP_PlayerController. Character input is not guaranteed to keep processing once Set Game Paused is active. A PlayerController is.

Closing is handled by WBP_PauseMenu's own On Preview Key Down override - not through a Controller bool - which sidesteps the state-desync bug that caused Tab to get stuck only ever opening.

WBP_HealthBar
WBP_InteractionPrompt
WBP_PauseMenu (Tab)

Roadmap

Unreal Engine 5.7

Must-Do - Studio-Impressive

  • Cinematic Dialogue + voice files - first, because Camp and Settler both depend on it
  • Camp trigger - random NPC visitor at rest point → dialogue → quest draw
  • Settler trigger - NPC in settlement → interact → dialogue → draw
  • Bounty Board - board actor → interact → draw (no dialogue needed)
  • Combat system - melee core, parry, dodge
  • Enemy NPC - patrol AI, squad tracking, interrogation (last enemy → draw)
  • Ally recruitment - the payoff the Quest Director was built to deliver

Complimentary - Complete Game

  • Inventory system - prerequisite for different weapons
  • Map system - prerequisite for Follow Quest marker
  • Enemy types and camps - liberating nation mechanic
  • Horse riding
  • Boss encounters
  • Multiple weapon types

Dependency Order

Dialogue System ↓ Camp + Settler + Bounty Board (all need Dialogue) ↓ Combat System ↓ Enemy NPC (needs Combat) ↓ Ally Recruitment (needs Quest + Enemy + Dialogue)