The Difficulty Problem
Most games handle difficulty wrong. Three preset tiers (Easy/Normal/Hard) assume all players struggle with the same things. They don't. One player finds combat trivial but puzzles impossible. Another breezes through puzzles but can't time dodges.
Good difficulty design is personal. It meets each player where they are and keeps them in the "flow channel" — challenged enough to stay engaged, not so overwhelmed they quit.
The Flow Channel
Challenge
▲
│ ╱ ANXIETY (too hard)
│ ╱
│ ╱ ● FLOW
│ ╱
│ ╱ BOREDOM (too easy)
└──────────────────────▶ Skill
Flow happens when challenge matches skill. As players improve, challenge must increase proportionally. The difficulty system's job is to keep each player in their personal flow channel.
Approach 1: Adaptive Difficulty (Director AI)
How It Works
The game monitors player performance and adjusts difficulty invisibly:
- Player dying frequently → reduce enemy damage, add health pickups
- Player dominating → increase enemy aggression, reduce resources
- Player stuck on puzzle → provide subtle hints, extend timers
Implementation
UCLASS()
class UDifficultyDirector : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Tick(float DeltaTime);
// Other systems query this for modifiers
UFUNCTION(BlueprintPure)
float GetDamageModifier() const; // 0.7 (easier) to 1.5 (harder)
UFUNCTION(BlueprintPure)
float GetResourceModifier() const;
private:
void UpdatePerformanceMetrics();
void AdjustDifficulty();
// Rolling averages
float RecentDeathRate; // Deaths per 10 minutes
float RecentDamageReceived; // Average damage taken per encounter
float RecentEncounterTime; // How long encounters take
float RecentResourceBalance; // Are they flush or starving?
// Current difficulty state (0.0 = easiest, 1.0 = hardest)
float CurrentDifficulty = 0.5f;
// Adjustment rate (how fast difficulty responds)
float AdjustmentSpeed = 0.1f; // Slow adjustments feel natural
};
The Key Principles
Invisible adjustments: Players should never notice the system working. If they feel patronized ("the game went easy on me"), the system failed.
Slow response: Adjust gradually over minutes, not seconds. A single death shouldn't trigger massive difficulty reduction — maybe the player just made a mistake.
Bias toward challenging: When in doubt, keep difficulty slightly above the player's current skill. Being slightly challenged is more engaging than being slightly bored.
Cap the range: Set minimum and maximum difficulty bounds. Even at minimum, the game should still present some challenge. At maximum, it should remain theoretically beatable.
Famous Examples
- Resident Evil 4: Pioneered adaptive difficulty. Enemy damage, count, and item drops adjust based on player performance. Most players never notice.
- Left 4 Dead: The AI Director controls zombie spawns, item placement, and pacing based on team performance.
- God of War (2018): Hidden difficulty adjustments on top of the player-selected difficulty tier.
Approach 2: Custom Difficulty Sliders
The Trend in 2026
Players increasingly want granular control. Instead of one difficulty dropdown, offer sliders for independent aspects:
Combat Difficulty: [||||||||--] 80%
Puzzle Difficulty: [||||------] 40%
Exploration Guidance: [||||||----] 60%
Resource Scarcity: [||||||||||] 100%
Time Pressure: [----------] Off
What to Make Adjustable
Combat: Enemy health, enemy damage, enemy count, player damage, healing effectiveness, dodge timing window
Navigation: Map detail, objective markers, hint frequency, waypoint visibility
Puzzles: Hint availability, timer leniency, skip option after X failed attempts
Resources: Drop rates, shop prices, crafting yields, durability drain
Time pressure: Timer presence/absence, timer leniency, pause-during-timers option
Implementation Pattern
USTRUCT(BlueprintType)
struct FDifficultySettings
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, meta=(ClampMin=0.0, ClampMax=2.0))
float EnemyDamageMultiplier = 1.0f;
UPROPERTY(EditAnywhere, meta=(ClampMin=0.0, ClampMax=2.0))
float EnemyHealthMultiplier = 1.0f;
UPROPERTY(EditAnywhere, meta=(ClampMin=0.5, ClampMax=2.0))
float PlayerDamageMultiplier = 1.0f;
UPROPERTY(EditAnywhere, meta=(ClampMin=0.0, ClampMax=2.0))
float ResourceDropMultiplier = 1.0f;
UPROPERTY(EditAnywhere)
bool bShowObjectiveMarkers = true;
UPROPERTY(EditAnywhere)
bool bEnableTimers = true;
UPROPERTY(EditAnywhere, meta=(ClampMin=0.5, ClampMax=2.0))
float TimerLeniency = 1.0f;
UPROPERTY(EditAnywhere)
EHintFrequency HintFrequency = EHintFrequency::Normal;
};
Presets as Starting Points
Offer presets that set all sliders at once, then let players customize:
- Story Mode: Low combat, high guidance, generous resources
- Balanced: Default values across the board
- Challenge: High combat, minimal guidance, scarce resources
- Custom: Player sets each slider independently
Approach 3: Assist Modes
Accessibility-First Design
Assist modes provide specific accommodations without changing the core game:
- Invincibility toggle: Player can't die (doesn't remove combat — enemies still react)
- Skip encounter: After N failures, option to skip and continue
- Slow motion: Global game speed reduction (0.7x, 0.5x)
- Auto-aim assist: Increased aim magnetism
- Extended timing windows: Parry, dodge, and QTE windows doubled
- Simplified controls: One-button combos, auto-block
- Navigation assist: Stronger objective guidance, highlighted paths
The Celeste Model
Celeste's assist mode is the gold standard:
- Every modifier is independent (slow motion + infinite dashes + invincibility, any combination)
- Clear explanation of what each modifier does
- No judgment — the game doesn't penalize players for using assists
- Achievements and progression are unaffected
Implementation
USTRUCT(BlueprintType)
struct FAssistSettings
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
bool bInvincible = false;
UPROPERTY(EditAnywhere, meta=(ClampMin=0.3, ClampMax=1.0))
float GameSpeed = 1.0f;
UPROPERTY(EditAnywhere, meta=(ClampMin=1.0, ClampMax=3.0))
float TimingWindowMultiplier = 1.0f;
UPROPERTY(EditAnywhere)
bool bAutoAim = false;
UPROPERTY(EditAnywhere)
int32 SkipEncounterAfterDeaths = 0; // 0 = disabled
UPROPERTY(EditAnywhere)
bool bNavigationAssist = false;
};
Combining Approaches
The best difficulty systems layer multiple approaches:
Layer 1: Player-selected preset (Easy/Normal/Hard)
Sets baseline values for all systems
Layer 2: Custom sliders (optional)
Player fine-tunes specific aspects
Layer 3: Adaptive adjustments (invisible)
Small adjustments within the selected tier
Never crosses tier boundaries without player consent
Layer 4: Assist modes (explicit opt-in)
Specific accommodations as needed
Dynamic Enemy Scaling
Encounter Budget System
Instead of static enemy placements, define encounters as budgets:
USTRUCT()
struct FEncounterDefinition
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
int32 BudgetPoints = 100;
UPROPERTY(EditAnywhere)
TMap<TSubclassOf<AEnemy>, int32> EnemyCosts;
// Grunt = 10 points, Elite = 25, Boss = 100
};
At lower difficulty, spend fewer budget points (spawn fewer/weaker enemies). At higher difficulty, spend more. The encounter design stays the same; only the population changes.
Rubber Banding
Subtle adjustments during combat:
- Health rubber banding: When player health is low, enemies deal slightly less damage (invisible 10-20% reduction)
- Hit probability: Missed attacks by the player can have slightly increased magnetism at lower difficulty
- Enemy hesitation: At lower difficulty, enemies pause slightly longer between attacks, giving players more reaction time
These adjustments must be invisible. If players notice, trust is broken.
Difficulty and Achievement
The Controversy
Should difficulty affect achievements, unlocks, or completion percentage?
Argument for gating: Higher difficulty is part of the game's intended challenge. Beating it on Hard is an achievement worth recognizing.
Argument against gating: Accessibility. Players who can't physically perform at Hard difficulty are excluded from content they paid for.
The 2026 consensus: All story content should be accessible at all difficulty levels. Optional cosmetic rewards or achievement badges can be difficulty-gated. Never gate gameplay content behind difficulty.
Testing Difficulty
Playtester Diversity
The most critical testing requirement: test with players across the skill spectrum:
- Experienced action game players
- Casual gamers
- Players new to the genre
- Players with motor disabilities
- Players who only use controllers (not mouse)
Metrics to Track
Deaths per encounter (by difficulty setting)
Time to complete each section (by difficulty setting)
Difficulty setting changes during playthrough (frustration signal)
Assist mode activation points (where do players need help?)
Quit points (where do players abandon the game?)
The Two-Player Test
Have two players of different skill levels play simultaneously. If both are engaged (neither bored nor frustrated), your difficulty system is working. If one is having fun and the other isn't, your range needs expansion.
Difficulty is not an afterthought bolted onto finished gameplay. It's a core system that determines whether your game reaches its full audience. Design it with the same care you give your combat, your levels, and your narrative.