Tactical Action · Features

The 2D Run-and-Gun (Arcade) Cell

A focused page within the Tactical Action Features documentation. The full map and every sibling page live in the Features hub.

5sections11 minread1diagram

On this page

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/V4TestsContraSpec, 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.

flowchart LR Input["V4Input · Enhanced Input"] --> Abilities subgraph Abilities["V4ArcadeAbilities · GAS (cell-gated: Arcade)"] Shoot[UGA_Shoot] Jump[UGA_Jump] Drop[UGA_DropPlatform] end subgraph Arc["V4Arcade — engine components"] Scroll["UV4SideScrollComponent<br/><sub>midpoint cam · 650 frame · 900 tether</sub>"] Lives["UV4LivesComponent<br/><sub>3 lives · 2 continues</sub>"] Power["UV4PowerUpComponent<br/><sub>×1.5 · drop on death</sub>"] Wpn["UV4WeaponPickupComponent<br/><sub>one weapon · S/L/M/F/C/H/B</sub>"] Ladder[UV4LadderClimbComponent] DropC[UV4DropPlatformComponent] Mini["UV4ArcadeMiniGameCatalog<br/><sub>7 Battle-Hub cabinets</sub>"] end subgraph Plug["V4Mode_Arcade_Contra / _BossGauntlet — plugins"] Contra["UV4Contra* models<br/><sub>stages · weapons · feel sim</sub>"] Boss["UV4BossGauntlet*<br/><sub>12 bosses · weekly chain · board</sub>"] end Shoot --> Wpn Shoot --> Power Jump --> Scroll Drop --> DropC Wpn --> Attr["UV4Attr_Arcade<br/><sub>Health · Lives · ScoreMultiplier</sub>"] Plug --> Arc Paper["Paper2D"] --> Arc Arc --> Modes["V4Modes<br/><sub>mode registry · GameFeature URLs</sub>"]

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.