
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 screenA 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
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);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"], }, });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()));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(); }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); });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));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; });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); });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");
// #endregionDesign 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
- 1979Origin
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) - 1985Commando
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) - 1985
Konami's Green Beret takes the Commando formula to side-scrolling.
Run and gun (Wikipedia) - 1986
Namco's Rolling Thunder adds cover mechanics to the formula.
Run and gun (Wikipedia) - 1987Contra
Konami's Contra stands out for its eight-direction aiming, including downwards in mid-jump, and its simultaneous two-player co-op.
Contra (Wikipedia) - 1993Gunstar Heroes
Treasure's Gunstar Heroes lets you combine its four shot types in pairs to create new weapons.
Gunstar Heroes (Wikipedia) - 1996Metal 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) - 2017Cuphead
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
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
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
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
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
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
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.