Skip to content
GameGenresEspañol
Shooter

Run and gun

You advance on foot through scrolling levels, running, jumping and firing nonstop at enemies coming from every side.

Also known as: run 'n' gun

Defining mechanics

  • Near-continuous fire while you move: running and shooting happen at the same time, never in turns.
  • Aiming in several directions (often eight) with the same stick or arrows you move with.
  • Platforms and jumps in the side-view variant, or an open field with vertical scrolling in the top-down variant.
  • Foot soldiers who rush at you and fixed turrets that fire aimed shots, often from above.
  • Weapons picked up along the way (spread, laser, limited ammo) and few lives, or a single hit to die.
  • Large bosses at the end of each level, with attack patterns you have to learn.

Gameplay loop

Move forward, see who appears and from where, aim there without stopping, dodge their shots and reach the boss at the end.

Our demo

Controls. Arrows or WASD to move and aim (up and diagonals; down only in mid-air), space, Z or K to jump, hold X or J to shoot. On mobile, the on-screen buttons.

Open full screen

A side-scrolling level with soldiers who run at you, turrets up high, a pit to jump over and a boss at the end. Just enough to feel what defines the genre, which is moving and shooting at the same time in several directions.

Made with Kaplay, in about 184 lines of JavaScript.

How it's made

  1. Setup and controls

    Virtual buttons are defined for the four directions, jumping and shooting. The arrows do two jobs at once, moving and aiming, as in single-stick arcade games. FIRE_RATE sets how many bullets come out per second and LIVES how many hits you can take.

    const k = kaplay({
      width: 640,
      height: 360,
      letterbox: true, pixelDensity,
      background: [28, 40, 56],
      buttons: {
        left: { keyboard: ["left", "a"] },
        right: { keyboard: ["right", "d"] },
        up: { keyboard: ["up", "w"] },
        down: { keyboard: ["down", "s"] },
        jump: { keyboard: ["space", "z", "k"] },
        shoot: { keyboard: ["x", "j"] },
      },
    });
    touchControls(k, [
      { button: "left", label: "◀" },
      { button: "up", label: "▲" },
      { button: "down", label: "▼" },
      { button: "right", label: "▶" },
      { button: "shoot", label: "•", side: "right" },
      { button: "jump", label: "⤒", side: "right" },
    ]);
    
    const TILE = 32;
    const SPEED = 200;
    const JUMP_FORCE = 560;
    const BULLET_SPEED = 520;
    const FIRE_RATE = 0.14; // seconds between shots while the button is held
    const LIVES = 5;
    const BOSS_X = 1600; // where the boss arena starts
    setGravity(1500);
  2. The level as text

    The level is a list of strings that addLevel turns into ground, blocks, soldiers (s) and turrets (T). The turrets sit on high platforms on purpose, so you have to aim upwards.

    // = ground · # block · s soldier · T turret. The boss arena is the last screen.
    const MAP = [
      "                                                                      ",
      "                                                                      ",
      "                                                                      ",
      "                                                                      ",
      "                                                                      ",
      "                            T           T                             ",
      "                          #####       #####                           ",
      "                                               ##                     ",
      "          ##  s          s       s          s  ## s                   ",
      "===================   ================================================",
      "===================   ================================================",
    ];
    const solid = (c) => [rect(TILE, TILE), color(c), area(), body({ isStatic: true }), "solid"];
    const buildLevel = () =>
      addLevel(MAP, {
        tileWidth: TILE,
        tileHeight: TILE,
        pos: vec2(0, 8),
        tiles: {
          "=": () => solid(rgb(80, 120, 90)),
          "#": () => solid(rgb(130, 110, 90)),
          s: () => [rect(22, 32), color(220, 80, 70), pos(5, 0), area(), body(), { hp: 1 }, "enemy", "soldier"],
          T: () => [rect(26, 26), color(240, 150, 60), pos(3, 6), area(), { hp: 3, reload: 1 }, "enemy", "turret"],
        },
      });
  3. The player

    Moving and jumping work as in a platformer. Each hit takes a life and gives 1.2 seconds of invulnerability, during which the character blinks; without that margin, several enemies together would take all your lives at once. Falling into the pit also makes you lose.

    const player = add([
      rect(22, 34), pos(64, 200), anchor("bot"), color(120, 200, 255), area(), body(), opacity(1),
      { dir: 1, lives: LIVES, safe: 0 },
    ]);
    onButtonDown("left", () => (player.move(-SPEED, 0), (player.dir = -1)));
    onButtonDown("right", () => (player.move(SPEED, 0), (player.dir = 1)));
    onButtonPress("jump", () => player.isGrounded() && player.jump(JUMP_FORCE));
    function hurt() {
      if (player.safe > 0) return;
      player.lives--;
      player.safe = 1.2; // a moment of invulnerability after each hit
      shake(5);
      if (player.lives <= 0) go("end", false);
    }
    player.onUpdate(() => {
      player.safe -= dt();
      player.opacity = player.safe > 0 && Math.floor(time() * 16) % 2 ? 0.3 : 1;
      hud.text = t({ es: "Vidas: ", en: "Lives: " }) + "♥".repeat(Math.max(0, player.lives));
      if (player.pos.y > 500) go("end", false);
    });
    player.onCollide("enemy", hurt);
    player.onCollide("ebullet", (b) => (destroy(b), hurt()));
  4. Aiming in eight directions

    aimDir() turns the arrows you hold into a vector: up, down, left, right and their combinations. If you hold none, you shoot where you face. Aiming down only works in mid-air, because on the ground the bullet would hit your feet. Finally the vector is normalised (unit()) so the diagonal is not faster than the horizontal.

    // Eight directions from the arrows you hold: up, diagonals, and down only in
    // mid-air. With no arrow held, you shoot where you face.
    function aimDir() {
      let x = (isButtonDown("right") ? 1 : 0) - (isButtonDown("left") ? 1 : 0);
      let y = (isButtonDown("down") ? 1 : 0) - (isButtonDown("up") ? 1 : 0);
      if (y > 0 && player.isGrounded()) y = 0;
      if (x === 0 && y === 0) x = player.dir;
      return vec2(x, y).unit();
    }
  5. Continuous fire

    While you hold the button a bullet comes out every FIRE_RATE seconds. Each bullet has move() in the aiming direction, is destroyed when it hits a block or leaves the screen (offscreen) and takes one health point from the enemy it touches.

    // Holding the button fires a steady stream: in this genre you almost never stop shooting.
    let cooldown = 0;
    onUpdate(() => {
      cooldown -= dt();
      if (!isButtonDown("shoot") || cooldown > 0) return;
      cooldown = FIRE_RATE;
      const d = aimDir();
      add([
        circle(4), color(255, 240, 120), anchor("center"), pos(player.pos.add(d.scale(18)).add(0, -20)),
        move(d, BULLET_SPEED), area(), offscreen({ destroy: true }), "bullet",
      ]);
    });
    onCollide("bullet", "solid", (b) => destroy(b));
    onCollide("bullet", "enemy", (b, e) => {
      destroy(b);
      e.hp--;
      if (e.hp > 0) return;
      if (e.is("boss")) go("end", true);
      else destroy(e);
    });
  6. Soldiers and turrets

    There are two enemies that call for different answers. Soldiers run at you along the ground and are dealt with by shooting straight ahead. Turrets do not move, but fire bullets aimed at your position every few seconds from up high, so you have to aim diagonally or upwards and dodge at the same time.

    // Soldiers run at you along the ground; turrets wait on ledges and fire aimed
    // shots, so you have to aim up and diagonally to clear them.
    function enemyShot(from, angleOffset = 0, speed = 200) {
      const d = player.pos.add(0, -17).sub(from).unit();
      const a = Math.atan2(d.y, d.x) + angleOffset;
      add([circle(5), color(255, 90, 90), anchor("center"), pos(from), move(vec2(Math.cos(a), Math.sin(a)), speed),
        area(), offscreen({ destroy: true }), "ebullet"]);
    }
    onUpdate("soldier", (s) => {
      if (Math.abs(s.pos.x - player.pos.x) < 420) s.move(s.pos.x > player.pos.x ? -90 : 90, 0);
      if (s.pos.y > 500) destroy(s);
    });
    onUpdate("turret", (tr) => {
      tr.reload -= dt();
      if (tr.reload <= 0 && Math.abs(tr.pos.x - player.pos.x) < 420) {
        tr.reload = 2.2;
        enemyShot(tr.worldPos().add(13, 13));
      }
    });
    onCollide("ebullet", "solid", (b) => destroy(b));
  7. The boss

    The boss appears when you enter the last screen. It moves up and down along a sine wave, so sometimes you can hit it head-on and sometimes you have to jump or aim diagonally. Every two seconds it fires a fan of three bullets at you. It takes 24 hits and the bar at the top shows how many are left.

    // The last screen locks the camera. The boss floats up and down, fires a fan
    // of three shots and only falls after many hits: a test of everything above.
    let boss = null;
    let bossTime = 0;
    const bossBar = add([rect(0, 10), pos(170, 18), color(240, 80, 80), fixed(), z(10)]);
    onUpdate(() => {
      if (!boss && player.pos.x > BOSS_X + 40) {
        boss = add([rect(80, 110), pos(BOSS_X + 560, 200), anchor("center"), color(170, 90, 200), area(),
          { hp: 24, reload: 2 }, "enemy", "boss"]);
      }
      if (!boss) return;
      bossTime += dt();
      boss.pos.y = 200 + Math.sin(bossTime * 1.3) * 80;
      boss.reload -= dt();
      if (boss.reload <= 0) {
        boss.reload = 2;
        for (const off of [-0.35, 0, 0.35]) enemyShot(boss.pos.sub(40, 0), off, 170);
      }
      bossBar.width = boss.hp * 12;
    });
  8. The camera and the final arena

    The camera follows the player horizontally until the boss arena; there it stays put and the player cannot go back. It is the same lock that almost every action game uses for boss fights.

    // The camera follows you until the boss arena, then stays put and walls you in.
    const levelWidth = MAP[0].length * TILE;
    player.onUpdate(() => {
      const arena = player.pos.x > BOSS_X;
      if (arena) player.pos.x = Math.max(player.pos.x, BOSS_X + 12);
      player.pos.x = Math.max(12, Math.min(player.pos.x, levelWidth - 12));
      const x = arena ? BOSS_X + 320 : Math.min(Math.max(player.pos.x, 320), levelWidth - 320);
      camPos(x, 180);
    });
  9. Scenes

    The game has three scenes, title, game and end, and go() jumps between them. Beating the boss or losing every life leads to end.

    function screen(title, subtitle, next) {
      add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 40)), anchor("center")]);
      add([
        text(subtitle, { size: 16, font: FONT, align: "center", width: 560 }),
        pos(center().add(0, 30)),
        anchor("center"),
        color(200, 210, 255),
      ]);
      onButtonPress("jump", next);
      onKeyPress("enter", next);
      onClick(next);
    }
    
    scene("title", () =>
      screen(
        t({ es: "Run and gun", en: "Run and gun" }),
        t({
          es: "Flechas para moverte y apuntar (arriba y diagonales), espacio o Z para saltar, mantén X para disparar. Llega al final y vence al jefe.\nPulsa espacio o toca para empezar.",
          en: "Arrows to move and aim (up and diagonals), space or Z to jump, hold X to shoot. Reach the end and beat the boss.\nPress space or tap to start.",
        }),
        () => go("game"),
      ),
    );
    
    scene("end", (won) =>
      screen(
        won ? t({ es: "¡Jefe derrotado!", en: "Boss defeated!" }) : t({ es: "Inténtalo otra vez", en: "Try again" }),
        t({ es: "Pulsa espacio para jugar otra vez.", en: "Press space to play again." }),
        () => go("game"),
      ),
    );
    
    go("title");

What we simplified

  • The character, the enemies and the boss are rectangles; there are no animations or sound.
  • There is a single weapon; there are no pickups and no grenades.
  • You have five lives and a moment of invulnerability after each hit; in the genre's classics a single hit is often enough to die.

To modify it

  • Add a pickup that changes the shot into a fan of three bullets.
  • Add a button that locks the firing direction while held, so you can move without stopping aiming diagonally.
  • Make the boss change pattern when it is down to half health, for example firing faster or coming down to the ground.
Full code
// Run and gun demo
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, touchControls, pixelDensity } from "../common.js";

// #region setup
const k = kaplay({
  width: 640,
  height: 360,
  letterbox: true, pixelDensity,
  background: [28, 40, 56],
  buttons: {
    left: { keyboard: ["left", "a"] },
    right: { keyboard: ["right", "d"] },
    up: { keyboard: ["up", "w"] },
    down: { keyboard: ["down", "s"] },
    jump: { keyboard: ["space", "z", "k"] },
    shoot: { keyboard: ["x", "j"] },
  },
});
touchControls(k, [
  { button: "left", label: "◀" },
  { button: "up", label: "▲" },
  { button: "down", label: "▼" },
  { button: "right", label: "▶" },
  { button: "shoot", label: "•", side: "right" },
  { button: "jump", label: "⤒", side: "right" },
]);

const TILE = 32;
const SPEED = 200;
const JUMP_FORCE = 560;
const BULLET_SPEED = 520;
const FIRE_RATE = 0.14; // seconds between shots while the button is held
const LIVES = 5;
const BOSS_X = 1600; // where the boss arena starts
setGravity(1500);
// #endregion

// #region level
// = ground · # block · s soldier · T turret. The boss arena is the last screen.
const MAP = [
  "                                                                      ",
  "                                                                      ",
  "                                                                      ",
  "                                                                      ",
  "                                                                      ",
  "                            T           T                             ",
  "                          #####       #####                           ",
  "                                               ##                     ",
  "          ##  s          s       s          s  ## s                   ",
  "===================   ================================================",
  "===================   ================================================",
];
const solid = (c) => [rect(TILE, TILE), color(c), area(), body({ isStatic: true }), "solid"];
const buildLevel = () =>
  addLevel(MAP, {
    tileWidth: TILE,
    tileHeight: TILE,
    pos: vec2(0, 8),
    tiles: {
      "=": () => solid(rgb(80, 120, 90)),
      "#": () => solid(rgb(130, 110, 90)),
      s: () => [rect(22, 32), color(220, 80, 70), pos(5, 0), area(), body(), { hp: 1 }, "enemy", "soldier"],
      T: () => [rect(26, 26), color(240, 150, 60), pos(3, 6), area(), { hp: 3, reload: 1 }, "enemy", "turret"],
    },
  });
// #endregion

scene("game", () => {
  buildLevel();
  const hud = add([text("", { size: 18, font: FONT }), pos(16, 12), fixed(), z(10)]);

  // #region player
  const player = add([
    rect(22, 34), pos(64, 200), anchor("bot"), color(120, 200, 255), area(), body(), opacity(1),
    { dir: 1, lives: LIVES, safe: 0 },
  ]);
  onButtonDown("left", () => (player.move(-SPEED, 0), (player.dir = -1)));
  onButtonDown("right", () => (player.move(SPEED, 0), (player.dir = 1)));
  onButtonPress("jump", () => player.isGrounded() && player.jump(JUMP_FORCE));
  function hurt() {
    if (player.safe > 0) return;
    player.lives--;
    player.safe = 1.2; // a moment of invulnerability after each hit
    shake(5);
    if (player.lives <= 0) go("end", false);
  }
  player.onUpdate(() => {
    player.safe -= dt();
    player.opacity = player.safe > 0 && Math.floor(time() * 16) % 2 ? 0.3 : 1;
    hud.text = t({ es: "Vidas: ", en: "Lives: " }) + "♥".repeat(Math.max(0, player.lives));
    if (player.pos.y > 500) go("end", false);
  });
  player.onCollide("enemy", hurt);
  player.onCollide("ebullet", (b) => (destroy(b), hurt()));
  // #endregion

  // #region aim
  // Eight directions from the arrows you hold: up, diagonals, and down only in
  // mid-air. With no arrow held, you shoot where you face.
  function aimDir() {
    let x = (isButtonDown("right") ? 1 : 0) - (isButtonDown("left") ? 1 : 0);
    let y = (isButtonDown("down") ? 1 : 0) - (isButtonDown("up") ? 1 : 0);
    if (y > 0 && player.isGrounded()) y = 0;
    if (x === 0 && y === 0) x = player.dir;
    return vec2(x, y).unit();
  }
  // #endregion

  // #region shooting
  // Holding the button fires a steady stream: in this genre you almost never stop shooting.
  let cooldown = 0;
  onUpdate(() => {
    cooldown -= dt();
    if (!isButtonDown("shoot") || cooldown > 0) return;
    cooldown = FIRE_RATE;
    const d = aimDir();
    add([
      circle(4), color(255, 240, 120), anchor("center"), pos(player.pos.add(d.scale(18)).add(0, -20)),
      move(d, BULLET_SPEED), area(), offscreen({ destroy: true }), "bullet",
    ]);
  });
  onCollide("bullet", "solid", (b) => destroy(b));
  onCollide("bullet", "enemy", (b, e) => {
    destroy(b);
    e.hp--;
    if (e.hp > 0) return;
    if (e.is("boss")) go("end", true);
    else destroy(e);
  });
  // #endregion

  // #region enemies
  // Soldiers run at you along the ground; turrets wait on ledges and fire aimed
  // shots, so you have to aim up and diagonally to clear them.
  function enemyShot(from, angleOffset = 0, speed = 200) {
    const d = player.pos.add(0, -17).sub(from).unit();
    const a = Math.atan2(d.y, d.x) + angleOffset;
    add([circle(5), color(255, 90, 90), anchor("center"), pos(from), move(vec2(Math.cos(a), Math.sin(a)), speed),
      area(), offscreen({ destroy: true }), "ebullet"]);
  }
  onUpdate("soldier", (s) => {
    if (Math.abs(s.pos.x - player.pos.x) < 420) s.move(s.pos.x > player.pos.x ? -90 : 90, 0);
    if (s.pos.y > 500) destroy(s);
  });
  onUpdate("turret", (tr) => {
    tr.reload -= dt();
    if (tr.reload <= 0 && Math.abs(tr.pos.x - player.pos.x) < 420) {
      tr.reload = 2.2;
      enemyShot(tr.worldPos().add(13, 13));
    }
  });
  onCollide("ebullet", "solid", (b) => destroy(b));
  // #endregion

  // #region boss
  // The last screen locks the camera. The boss floats up and down, fires a fan
  // of three shots and only falls after many hits: a test of everything above.
  let boss = null;
  let bossTime = 0;
  const bossBar = add([rect(0, 10), pos(170, 18), color(240, 80, 80), fixed(), z(10)]);
  onUpdate(() => {
    if (!boss && player.pos.x > BOSS_X + 40) {
      boss = add([rect(80, 110), pos(BOSS_X + 560, 200), anchor("center"), color(170, 90, 200), area(),
        { hp: 24, reload: 2 }, "enemy", "boss"]);
    }
    if (!boss) return;
    bossTime += dt();
    boss.pos.y = 200 + Math.sin(bossTime * 1.3) * 80;
    boss.reload -= dt();
    if (boss.reload <= 0) {
      boss.reload = 2;
      for (const off of [-0.35, 0, 0.35]) enemyShot(boss.pos.sub(40, 0), off, 170);
    }
    bossBar.width = boss.hp * 12;
  });
  // #endregion

  // #region camera
  // The camera follows you until the boss arena, then stays put and walls you in.
  const levelWidth = MAP[0].length * TILE;
  player.onUpdate(() => {
    const arena = player.pos.x > BOSS_X;
    if (arena) player.pos.x = Math.max(player.pos.x, BOSS_X + 12);
    player.pos.x = Math.max(12, Math.min(player.pos.x, levelWidth - 12));
    const x = arena ? BOSS_X + 320 : Math.min(Math.max(player.pos.x, 320), levelWidth - 320);
    camPos(x, 180);
  });
  // #endregion
});

// #region scenes
function screen(title, subtitle, next) {
  add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 40)), anchor("center")]);
  add([
    text(subtitle, { size: 16, font: FONT, align: "center", width: 560 }),
    pos(center().add(0, 30)),
    anchor("center"),
    color(200, 210, 255),
  ]);
  onButtonPress("jump", next);
  onKeyPress("enter", next);
  onClick(next);
}

scene("title", () =>
  screen(
    t({ es: "Run and gun", en: "Run and gun" }),
    t({
      es: "Flechas para moverte y apuntar (arriba y diagonales), espacio o Z para saltar, mantén X para disparar. Llega al final y vence al jefe.\nPulsa espacio o toca para empezar.",
      en: "Arrows to move and aim (up and diagonals), space or Z to jump, hold X to shoot. Reach the end and beat the boss.\nPress space or tap to start.",
    }),
    () => go("game"),
  ),
);

scene("end", (won) =>
  screen(
    won ? t({ es: "¡Jefe derrotado!", en: "Boss defeated!" }) : t({ es: "Inténtalo otra vez", en: "Try again" }),
    t({ es: "Pulsa espacio para jugar otra vez.", en: "Press space to play again." }),
    () => go("game"),
  ),
);

go("title");
// #endregion

Design challenges

  • Keeping the screen readable: enemy bullets in a colour and size different from your own and from the background.
  • Solving aiming with few buttons: deciding whether moving and aiming share the same arrows, whether there is a button to lock direction, or whether you aim with the mouse.
  • Placing enemies and turrets so that they force you to use every firing direction, not just the horizontal one.
  • Designing a boss that tests what the level taught, with clear warnings before each attack.

History

  1. 1979
    Origin

    Its origins go back to Sheriff (Nintendo, 1979); the genre became popular in the mid-1980s with military-themed games such as Commando (1985).

    Run and gun (Wikipedia)
  2. 1985
    Commando

    Capcom's Commando, with a soldier who fires in eight directions and throws grenades across a vertically scrolling field, sets the genre's formula and popularises it.

    Commando (Wikipedia)
  3. 1985

    Konami's Green Beret takes the Commando formula to side-scrolling.

    Run and gun (Wikipedia)
  4. 1986

    Namco's Rolling Thunder adds cover mechanics to the formula.

    Run and gun (Wikipedia)
  5. 1987
    Contra

    Konami's Contra stands out for its eight-direction aiming, including downwards in mid-jump, and its simultaneous two-player co-op.

    Contra (Wikipedia)
  6. 1993
    Gunstar Heroes

    Treasure's Gunstar Heroes lets you combine its four shot types in pairs to create new weapons.

    Gunstar Heroes (Wikipedia)
  7. 1996
    Metal Slug

    Metal Slug, for the Neo Geo, adds melee attacks, limited-ammo weapons and a tank you can drive, and starts a series that keeps getting new entries.

    Metal Slug (Wikipedia)
  8. 2017
    Cuphead

    Studio MDHR's Cuphead centres the genre on boss fights, with hand-made animation in the style of 1930s cartoons.

    Cuphead (Wikipedia)

Reference games

1985

Commando

Capcom (Tokuro Fujiwara). arcade, NES, Commodore 64, ZX Spectrum

A soldier advances on foot across a vertically scrolling battlefield, firing in eight directions and lobbing grenades at what lies ahead. It popularised the run and gun and had many imitators.

1987

Contra

Konami. arcade, NES

Two soldiers run and jump through side-scrolling levels, firing in eight directions, including downwards in mid-jump. Its simultaneous two-player co-op was the key to its popularity.

1993

Gunstar Heroes

Treasure. Mega Drive

A frantic run and gun in which the four shot types combine in pairs to create new weapons, such as a machine gun with homing shots.

1996

Metal Slug

Nazca Corporation. arcade, Neo Geo

You advance shooting at an army, with melee attacks when an enemy is close, powerful weapons with limited ammo and a small tank you can climb into. It started a long-running series known for its humour.

2017

Cuphead

Studio MDHR. Windows, Xbox One, Nintendo Switch, PlayStation 4

A run and gun built almost entirely around boss fights, interspersed with run-and-gun levels. Everything is hand-animated in the style of 1930s cartoons.

2019

Captain Photon and the planet of chaos

Shiv Games. navegador

A short Contra-style run and gun made in Construct for a game jam. You aim with the arrows, can lock your firing direction and jump between platforms.

Exercise

Play the demo, then change FIRE_RATE and the number of lives (LIVES) and note how the difficulty changes. Then add a weapon pickup that fires three bullets in a fan, like the classic spread shot.

Sources