This is V4's arcade machine. The 2D Run-and-Gun cell is the Contra-shaped surface where the moment-to-moment loop is run, jump, drop through a platform, grab a power-up, and never lose the second player off the edge of the screen — the genre that lives or dies on snappy controls, a readable side-scroll camera, and the merciless one-hit-kill economy that makes three lives and two continues feel like a real stake. It is the smallest of V4's six genre modules and the catalogue says so plainly — "the smallest cell, but mandatory at launch" — but it is also one of the most complete: the camera math, the S/L/M/F/C/H/B weapon roster, the lives-and-continues ledger, and an eight-stage campaign are real, compiled C++ pinned by automation, and the cell's 60-fps two-player budget is verified by an honest-to-goodness timed simulation rather than a stat sheet.
V4 ships this cell as two hot-swappable GameFeatures plugins on top of one
shared V4Arcade engine module: V4Mode_Arcade_Contra (the side-scrolling
campaign) and V4Mode_Arcade_BossGauntlet (the weekly boss-rush). The promise
is the same per-ruleset feel the rest of V4 chases — a frantic couch co-op run
must read as Contra, not as a 2D skin on the tactical shooter — yet it sits on
the same GAS spine and the same mode machinery as the breach-and-clear cell.
This page inventories what that arcade core actually does on the player's side
of the screen, and points at the exact Unreal C++ that backs each feature. For
the full mode taxonomy, roster, and the scope this slots into, start at the hub:
../V4_features.md.
What ships, honestly#
The run-and-gun logic is real, compiled, and tested. The engine module
V4/ue/Source/V4Arcade carries nine .cpp / nine .h of domain logic — a
side-scroll camera component, a lives/continues ledger, a power-up multiplier, a
weapon-pickup swap, drop-through-platform and ladder-climb components, three
cell-gated GAS abilities, and a Battle-Hub mini-game catalog — and the
V4Mode_Arcade_Contra plugin layers an authoring-and-validation model on top
(movement config, 8-way aim quantizer, health rules, co-op rules, a seven-weapon
catalog, an eight-stage catalog, and a feel simulator). The signature numbers
are designer-editable, not placeholders: StartingLives = 3 /
StartingContinues = 2, a 1.5× powered-up fire-rate, a
CoOpSeparationCap = 900 co-op tether, a ClimbSpeed = 300. Most are pinned by
dedicated specs under V4/ue/Source/V4Tests — ContraSpec, LivesSpec,
WeaponSpec, MiniGameSuiteSpec, BossGauntletSpec, plus the GAS integration
in AbilityClassSpec.
Three honest qualifications, in the spirit of the architecture companion.
First, this is the smallest cell by code volume, and the design catalogue
labels several systems (branching routes, four-player online co-op) as authored
rules and metadata rather than fully simulated runtimes. Second, the logic
is in-tree C++ but the content is not: V4 ships JSON-described content and
soft-object paths, not cooked .uasset binaries, so the Paper2D flipbook
sprites, parallax materials, tilemaps, and Niagara 2D projectiles the cell
references are described, not baked — V4Arcade.Build.cs honestly links
Paper2D so the 2D pipeline is wired at the dependency level. Third, the
Battle-Hub arcade cabinets (pinball, pool, darts) are a validated catalog of
definitions whose physics — flippers, cue, puck — are named RequiredSystemIds,
not in-tree cabinet simulations. What follows is the run-and-gun engine on its
own terms.
The 2D run-and-gun experience — what a run feels like#
Strip the two plugins away and the shared verb set is small, fast, and tactile: run a stage, jump variable-height, slide a wall, drop through a one-way platform, climb a ladder, aim in eight directions, and swap whatever power-up you just ran over — all while a co-op camera refuses to let you abandon player two. Each verb is either a component on the arcade pawn or a function on the Contra model, and each carries the specific tuning the genre argues about.
The side-scroll camera and the co-op tether#
UV4SideScrollComponent
(V4/ue/Source/V4Arcade/Private/V4SideScrollComponent.cpp) is the camera brain.
CalculateCameraLocation follows a single player directly, or — for couch co-op
— frames the midpoint of two: it takes the min and max player X, caps the
span at CoOpSeparationCap = 900 so the frame can't stretch unboundedly,
centres on that span, averages the players' Z, and then clamps the focus inside
the authored stage so Focus.X stays within
[StageMinX + HalfScreenWidth, StageMaxX - HalfScreenWidth] with
HalfScreenWidth = 650. The matching ClampPlayerToScreen keeps each player
inside the live frame — it derives the camera's focus X back out of the
CameraOffset = (-900, 0, 250), clamps the player to ±650 of it, then to the
stage bounds. That pair is the exact "you can't run off and leave player two
behind" constraint the genre is built on. The plugin's
UV4ContraSideScrollModel mirrors the same math for authoring
(CameraOffset = (-850, 0, 260), HalfScreenWidth = 640) and adds
QuantizeAim8Way, which Atan2s a raw stick vector, rounds to the nearest 45°
sector, wraps it into 0…7, and returns a re-normalized direction —
ContraSpec checks a (0.7, 0.7) diagonal snaps to direction index 1 and
stays unit-length.
Lives, continues, and the one-hit-kill economy#
UV4LivesComponent (V4/ue/Source/V4Arcade/Private/V4LivesComponent.cpp) is
the arcade ledger. ApplyHit refuses to act once the game is over, otherwise
decrements Lives toward a floor of zero, broadcasts OnLivesChanged, and
returns whether the player is still alive; UseContinue only fires when lives
have hit zero and continues remain, spends one continue, and restores Lives
to StartingLives; IsGameOver is true only when both lives and continues are
exhausted. LivesSpec walks the whole ladder — three lives, three hits to zero,
"not game over while continues remain," then a continue that restores three
lives and decrements the continue count to one. The Contra plugin's
UV4ContraHealthModel defines the rule the lives sit under: BuildHealthRules
returns bOneHitKillDefault = true with a HitCapacity of 1, and the
accessibility branch (bOneExtraHitEnabled) bumps capacity to 2 so a new
player survives the first contact per life — IsDeathOnHit(HitsRemaining)
resolves death the instant HitsRemaining <= 1. One-hit-kill by default, one
forgiving extra hit when toggled.
Weapons and the powered-up state#
Power-ups are the genre's whole progression curve, and the roster is
unmistakably Contra. EV4ArcadeWeaponType
(V4/ue/Source/V4Arcade/Public/V4ArcadeTypes.h) is the literal S/L/M/F/C/H/B
set — Spread, Laser, MachineGun, Fire, Crush, Homing, Barrier. The plugin's
UV4ContraWeaponCatalog::BuildWeapons gives each one real combat data: Spread
is a five-projectile fan (FireRate 4.5, ProjectileCount 5), the Laser is a
single high-damage shot (3.0, 1, Damage 4.0), the Machine Gun is the
narrow rapid default (9.0), Crush is the slow heavy hitter (Damage 6.0), and
Barrier carries no fire data at all because it is the temporary shield, not a
gun. UV4WeaponPickupComponent enforces "one weapon at a time": PickupWeapon
hands back the previously held spec as the dropped weapon, installs the new one,
and broadcasts the swap — WeaponSpec proves picking up a Laser drops the
default Machine Gun and a later Spread keeps its five-projectile fan data. The
powered-up state is its own component: UV4PowerUpComponent::ApplyFireRate
returns BaseFireRate × 1.5 while powered and the base rate otherwise, and
ClearOnDeath drops the powered state on a death — the exact "one death and
your yellow gun reverts" rule, verified in both ContraSpec and WeaponSpec.
Traversal — jump, drop-through, and ladders#
Platforming is three small, honest components.
UV4DropPlatformComponent::BeginDropThrough arms a DropThroughSeconds = 0.25
window during which the pawn ignores one-way collision, ticked down by
TickDropThrough; UV4LadderClimbComponent::ApplyClimbInput only moves while
bOnLadder, then clamps CurrentZ + AxisValue × ClimbSpeed × DeltaSeconds
between the ladder's authored MinZ and MaxZ (ClimbSpeed = 300); and the
plugin's BuildMovementConfig publishes the run-and-jump tuning the catalogue
promises — RunSpeed = 760, JumpVelocity = 980, WallSlideSpeed = 180, with
run, jump, wall-slide, and drop-through all enabled. ContraSpec asserts all
four verbs are on and that run and jump carry non-zero authored values.
The systems behind the two faces#
The arcade cell wears two faces from one module. The first is the Contra side-scrolling campaign; the second is the Battle-Hub arcade cabinets. Both are real C++; they differ in how much of the runtime is simulated versus authored.
Contra — the eight-stage campaign and the feel gate#
UV4ContraStageCatalog::BuildStages
(V4/ue/Plugins/V4Mode_Arcade_Contra/Source/V4Mode_Arcade_Contra/Private/V4ContraSystems.cpp)
authors the full launch campaign: eight named stages — Jungle Approach, Base
Interior, Waterfall Climb, Snowfield Relay, Fortress Runway, Alien Lair, City
Siege, Final Core — each with two or three branching routes that meet at a
boss arena, and a ValidateLaunchContent linter that rejects any stage with the
wrong route count, a mismatched route-id list, or a missing boss arena.
UV4ContraCoopModel encodes the co-op contract: 2P local couch is
bLaunchMandatory with RequiredLaunchLocalPlayers = 2, 4P online co-op is
supported (a fifth online player is rejected), and ShouldApplyScreenEdgeTether
fires only when separation exceeds the MaxTetherDistance = 900.
The standout is the feel gate.
UV4ContraFeelModel::CaptureTwoPlayer60FPSProfile is not a hardcoded "60 fps:
pass" — it runs a real two-player co-op frame loop and measures it. Inside the
loop it advances both players at the real RunSpeed, drives the actual
UV4ContraSideScrollModel camera and screen-clamp, applies the co-op tether,
spawns enemy waves from a deterministic LCG so every capture replays the
identical battle, fires both players' real weapon-catalog weapons at their real
fire rates (through QuantizeAim8Way), integrates and collides every
projectile, and resolves one-hit deaths that drop the powered state — all while
timing each frame with FPlatformTime::Seconds(). It then reports the measured
average, p95, and max frame times and sets bPasses only when
P95FrameMs <= 16.67. ContraSpec asserts the profile is bMeasured, that it
captured every one of 600 frames, that p95 never exceeds max, and that the pass
verdict equals the measured budget comparison — a test that would fail
instantly against a fabricated metric. The result is then fed into the shared
UV4FeelTestSuite gate as the TwoPlayerP95FrameMs metric, the same cross-cell
feel-test harness every other cell registers against.
Boss Gauntlet — the weekly rotation and the no-death board#
V4Mode_Arcade_BossGauntlet is the boss-rush face.
UV4BossGauntletCatalog::BuildBossPool authors twelve bosses (Iron Hydra
through the final-flagged Void Emperor at difficulty tier 7), and
BuildWeeklyRotatingChain deterministically picks a five-boss chain for a given
week by walking the pool from WeekIndex % PoolSize in stride-two steps — so
the chain is stable within a week but rotates between weeks, exactly as
BossGauntletSpec checks (week 0 equals itself, week 0's lead boss differs from
week 1's). The leaderboard is the genre's bragging-rights layer:
UV4BossGauntletLeaderboard::SubmitClear rejects any run that is not a clean
no-death clear (a death count above zero or a non-positive clear time is
refused), dedups by week-and-account, and keeps the board sorted by clear time;
GetTopNoDeathClears returns the fastest hundred no-death runs for a week. The
spec confirms a death-bearing run is rejected and the fastest clean clear ranks
first.
The Battle-Hub arcade cabinets#
The second face is social: UV4ArcadeMiniGameCatalog
(V4/ue/Source/V4Arcade/Private/V4ArcadeMiniGameCatalog.cpp) defines the seven
launch cabinets that live in the persistent lobby — two pinball machines (Astra
Circuit and Contra Multiball), plus air hockey, mini-golf, darts, eight-ball
pool, and a co-op cooking game — each with a score target, a player cap, a
Battle-Hub map and widget soft-path, and a list of RequiredSystemIds.
ValidateMiniGameSuite enforces the shape: exactly seven entries, at least two
pinball tables, exactly one of each other type, and every entry complete with at
least three system ids. This layer is honest about its maturity — it is a
validated catalog of cabinet definitions, and the cabinet physics those system
ids name (System.Pinball.Flippers, System.Pool.CuePhysics) are referenced,
not implemented in-tree. MiniGameSuiteSpec resolves Contra Multiball by id and
the cooking game by cabinet id and confirms the suite validates.
How the arcade cell ties to GAS and the shared engine#
Every arcade action routes through the Gameplay Ability System, and the cell's
three abilities in V4ArcadeAbilities.cpp are cell-gated so they cannot
fire on a non-arcade pawn — each sets RequiredCell = EV4RulesetCell::Arcade
(V4/ue/Source/V4Core/Public/V4CellState.h) and funnels real work into the
shared UV4GameplayAbilityBase::ExecuteV4Ability seam
(V4/ue/Source/V4Gameplay/Public/V4GameplayAbilities.h). UGA_Shoot finds the
pawn's weapon-pickup and power-up components, computes the effective fire rate,
and fails loud with "arcade-weapon-invalid" when the held weapon has no
rate, projectiles, or damage (or "missing-arcade-weapon" when the component is
absent) — it honestly reports what it could not do instead of faking a shot.
UGA_Jump raises the pawn by JumpHeight = 120 with a swept world offset, and
UGA_DropPlatform arms the drop-through window, each reporting a precise
rejection tag on failure. AbilityClassSpec does this for real: it spawns an
arcade pawn, attaches the components, grants and activates all three abilities,
and asserts the jump actually raised the pawn's Z and the drop actually started
— GAS integration, not stubs.
Underneath, the cell carries its own replicated attribute set. UV4Attr_Arcade
(V4/ue/Source/V4Gameplay/Public/V4AttributeSets.h) holds Health, Lives,
and a ScoreMultiplier, driven by real gameplay effects (UGE_V4_ArcadeDamage,
UGE_V4_ArcadeExtraLife, UGE_V4_ArcadeScoreMultiplierBoost) — so an
extra-life pickup or a score-multiplier boost is a GAS effect on the same spine
the other cells use. Both plugins register themselves through the mode
machinery: UV4ContraModeLibrary::BuildModeDefinition and the Boss-Gauntlet
equivalent publish FV4ModeDefinition rows (Arcade.Contra,
Arcade.BossGauntlet) with GameFeaturePlugin URLs and soft-pathed entry maps
and HUDs — described content, not baked binaries, but real registration into
V4Modes. That throughline is the architecture's unifying claim: six genres
stay one game because even the smallest cell sits on the same GAS spine, the
same shared mode registry, and the same feel-test harness. For the engine-side
treatment — the GAS layout, mode machinery, and the cross-cell seams named here
— read the architecture companion,
../architecture/per-cell-deep-dives.md.
Related#
- The feature hub: ../V4_features.md
- Action RPG (Wukong) cell — the third-person souls-like surface that, like this cell, is animation- and feel-led but trades arcade pacing for stamina-economy combat
- Shared cross-cell engine — the GAS spine, mode machinery, feel-test harness, and netcode authority every cell sits on
- Architecture companion:
../architecture/per-cell-deep-dives.md
— the genre-by-genre engine view, including
V4Arcadematurity