How to Make a Fighting Game
How to Vibe Code an Arcade Fighting Game
Stay in your creative flow. Direct AI models with high-level combat design intent, tune attack hitboxes and super meters, and ship a responsive 1v1 fighting game in your browser in under 25 minutes.
Core Stack
HTML5 Canvas + JS
Combat Engine
Hitboxes & Frame Data
AI Partner
Claude / Gemini
Target Time
15β25 Mins
The Vibe Coding Fighting Philosophy
Making a fighting game used to mean months of rigging skeletal animations, writing complex collision solvers, and hand-coding frame-by-frame state machines. Vibe coding puts you in the role of Combat Director: you define the startup/recovery feel, block stun, combo chains, and AI aggression while conversational AI generates the hitbox/hurtbox loop and arcade HUD.
Old-School Fighter Stress
- Getting stuck importing 200+ FBX animation clips before landing a single punch.
- Attacks registering damage on every single frame instead of once per swing.
- Writing brittle input buffer parsers that drop special move commands.
- AI opponents that either stand still or read your inputs with impossible perfection.
The Vibe Coder Way
- Expressive procedural fighters on HTML5 Canvas with clear attack telegraphs and hit sparks.
- Describe the combat vibe: "Light jab, heavy kick knockback, directional block, and a 100% Super Beam."
- Tune balance conversationally: "Add 3 frames of hitstop freeze when a heavy kick lands."
- Playtest VS AI or 2-Player Local Keyboard immediately in one browser tab.
The 4-Step Blueprint
From an empty arena to a 60-second arcade showdown with combos, blocking, and KO announcements.
Spawn the Arena Canvas
Create a single index.html file. A 2D HTML5 Canvas arena lets you visualize hurtboxes (green) and active attack hitboxes (red) with crisp precision.
The "One-Shot Architect" Prompt
Prompt your AI with the classic arcade layout: dual top health bars, 60-second round timer, Super gauge, Light/Heavy/Special attacks, and a reactive AI challenger.
hasHit flag so one swing only damages the opponent once."
Hitstop, Knockback & Combo Counters
Inject arcade crunch: brief hitstop frames on heavy hits, reduced chip damage while blocking, floating combo counters, and screen shake on K.O.
Playtest & Local 2-Player Mode
Add a toggle between VS CPU and 2-Player Local Keyboard (WASD vs. Arrow keys), test corner pressure with friends, and host your fighter on the web.
Vibe Prompt Configurator
Pick your arena style, opponent mode, and combat mechanics. Copy the generated prompt into Claude, Cursor, or Gemini.
Essential Elements of a Fighting Game
Interpersonal 1v1 combat lives and dies by frame responsiveness. Make sure your fighting game nails these six core pillars:
-
Expressive Character Animation
Clear startup, active, and recovery poses let players read incoming punches, kicks, jumps, and blocks without feeling stiff or lifeless.
-
Precision Collision Detection
Separate each fighter's body hurtbox from their weapon/fist hitbox so spacing, footsies, and anti-air attacks feel accurate.
-
Balanced Fighting Game AI
Design AI opponents that approach, block, and punish openings with human-like reaction delays so solo matches are challenging but fair.
-
Combos & Super Moves
Reward offensive pressure and meter management with fast Light-into-Heavy chains and high-impact Super moves when the energy bar fills.
-
Health Bars & Round Clock
Anchor the top of the screen with draining health gauges, round win pips, and a 60-second timer that decides timeout victories.
-
Defensive Options & Hitstun
Give players a reliable Guard/Block button, pushback on blocked hits, and brief hitstun so combat flows like a tactical conversation.
The "Golden" Single-File Fighting Game Template
Save this as index.html and open it in any browser to play a 1v1 Arcade Fighter with Light/Heavy attacks, Blocking, Super Beams, and AI.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vibe Fighter: Neon Dojo</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; }
body {
background: #090d16;
color: #f8fafc;
font-family: system-ui, -apple-system, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 12px;
}
.controls {
margin-bottom: 10px;
padding: 8px 16px;
background: #131c2e;
border: 1px solid #263554;
border-radius: 999px;
font-size: 13px;
color: #cbd5e1;
}
canvas {
border: 2px solid #f43f5e;
border-radius: 14px;
box-shadow: 0 20px 50px rgba(244, 63, 94, 0.2);
max-width: 100%;
height: auto;
background: #111827;
}
</style>
</head>
<body>
<div class="controls">
<strong>Vibe Fighter π₯</strong> β Move: <kbd>A</kbd>/<kbd>D</kbd> β’ Jump: <kbd>W</kbd> β’ Block: <kbd>S</kbd> β’ LightJab: <kbd>J</kbd> β’ HeavyKick: <kbd>K</kbd> β’ SuperWave (100% Meter): <kbd>L</kbd>
</div>
<canvas id="game" width="880" height="460"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const FLOOR_Y = 390;
const keys = {};
window.addEventListener('keydown', e => {
keys[e.code] = true;
if (state.over && e.code === 'Space') initMatch();
if (!state.over) {
if (e.code === 'KeyJ') startAttack(state.p1, 'light');
if (e.code === 'KeyK') startAttack(state.p1, 'heavy');
if (e.code === 'KeyL') startAttack(state.p1, 'super');
}
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
let state = {};
function makeFighter(x, color, name) {
return {
name, x, y: FLOOR_Y - 92, w: 46, h: 92,
vx: 0, vy: 0, color,
hp: 100, displayHp: 100, meter: 40,
facing: 1, blocking: false,
hitstun: 0, combo: 0,
atk: null // { type, timer, maxTimer, dmg, reach, knock, hasHit }
};
}
function initMatch() {
state = {
p1: makeFighter(180, '#38bdf8', 'KAI (YOU)'),
p2: makeFighter(650, '#f43f5e', 'RYUJI (AI)'),
timer: 60,
frameCount: 0,
hitstop: 0,
shake: 0,
sparks: [],
over: false,
banner: ''
};
}
function startAttack(f, type) {
if (f.atk || f.hitstun > 0 || f.blocking) return;
if (type === 'super') {
if (f.meter < 100) return;
f.meter = 0;
f.atk = { type: 'super', timer: 24, maxTimer: 24, dmg: 26, reach: 170, knock: 16, hasHit: false };
} else if (type === 'heavy') {
f.atk = { type: 'heavy', timer: 20, maxTimer: 20, dmg: 14, reach: 78, knock: 11, hasHit: false };
} else {
f.atk = { type: 'light', timer: 12, maxTimer: 12, dmg: 7, reach: 58, knock: 5, hasHit: false };
}
}
function spawnSparks(x, y, color, count) {
for (let i = 0; i < count; i++) {
state.sparks.push({
x, y,
vx: (Math.random() - 0.5) * 10,
vy: (Math.random() - 0.5) * 10,
color,
life: 16
});
}
}
function updateFighter(f, opp) {
f.displayHp += (f.hp - f.displayHp) * 0.12;
if (f.atk === null) {
f.facing = (opp.x >= f.x) ? 1 : -1;
}
if (f.hitstun > 0) f.hitstun--;
if (f.atk) {
f.atk.timer--;
// Active frames in the middle of the swing
const active = f.atk.timer <= f.atk.maxTimer - 3 && f.atk.timer >= 4;
if (active && !f.atk.hasHit) {
const hx = f.facing === 1 ? f.x + f.w : f.x - f.atk.reach;
const hy = f.y + 18;
const hw = f.atk.reach;
const hh = 45;
if (hx < opp.x + opp.w && hx + hw > opp.x && hy < opp.y + opp.h && hy + hh > opp.y) {
f.atk.hasHit = true;
const blocked = opp.blocking && opp.facing !== f.facing;
const dmg = blocked ? Math.ceil(f.atk.dmg * 0.2) : f.atk.dmg;
opp.hp = Math.max(0, opp.hp - dmg);
f.meter = Math.min(100, f.meter + (blocked ? 6 : 16));
opp.meter = Math.min(100, opp.meter + 8);
opp.vx = f.facing * (blocked ? f.atk.knock * 0.4 : f.atk.knock);
if (!blocked) {
opp.hitstun = f.atk.type === 'light' ? 10 : 18;
f.combo++;
state.hitstop = f.atk.type === 'light' ? 2 : 5;
state.shake = f.atk.type === 'super' ? 12 : 6;
}
spawnSparks(opp.x + opp.w / 2, hy + 20, blocked ? '#38bdf8' : '#facc15', blocked ? 6 : 14);
}
}
if (f.atk.timer <= 0) f.atk = null;
}
if (opp.hitstun === 0) f.combo = 0;
f.x += f.vx;
f.vx *= 0.78;
f.vy += 0.72;
f.y += f.vy;
if (f.y + f.h >= FLOOR_Y) {
f.y = FLOOR_Y - f.h;
f.vy = 0;
}
f.x = Math.max(24, Math.min(canvas.width - f.w - 24, f.x));
}
function updateAI(ai, p1) {
if (ai.hitstun > 0 || state.over) return;
const dist = Math.abs(p1.x - ai.x);
// Defensive reaction when player attacks up close
if (p1.atk && dist < 110 && Math.random() < 0.35) {
ai.blocking = true;
return;
}
ai.blocking = false;
if (dist > 72) {
ai.vx = ai.facing * 3.6;
if (Math.random() < 0.015 && ai.y + ai.h >= FLOOR_Y) ai.vy = -13.5;
} else if (!ai.atk && Math.random() < 0.12) {
if (ai.meter >= 100) startAttack(ai, 'super');
else if (Math.random() < 0.4) startAttack(ai, 'heavy');
else startAttack(ai, 'light');
}
}
function update() {
if (state.over) return;
if (state.hitstop > 0) {
state.hitstop--;
return;
}
state.frameCount++;
if (state.frameCount % 60 === 0 && state.timer > 0) state.timer--;
const p1 = state.p1;
const p2 = state.p2;
// Player controls
p1.blocking = Boolean(keys['KeyS']);
if (p1.hitstun === 0 && !p1.atk && !p1.blocking) {
if (keys['KeyA']) p1.vx = -4.8;
if (keys['KeyD']) p1.vx = 4.8;
if (keys['KeyW'] && p1.y + p1.h >= FLOOR_Y) p1.vy = -14;
}
updateAI(p2, p1);
updateFighter(p1, p2);
updateFighter(p2, p1);
// Prevent body overlap
if (Math.abs(p1.x - p2.x) < 42 && Math.abs(p1.y - p2.y) < 70) {
const push = p1.x < p2.x ? -2.5 : 2.5;
p1.x += push;
p2.x -= push;
}
// Check K.O. or Time Over
if (p1.hp <= 0 || p2.hp <= 0 || state.timer <= 0) {
state.over = true;
if (p1.hp === p2.hp) state.banner = 'DOUBLE K.O. / DRAW!';
else if (p1.hp > p2.hp) state.banner = 'K.O.! YOU WIN!';
else state.banner = 'K.O.! RYUJI WINS!';
}
state.sparks.forEach(s => {
s.x += s.vx;
s.y += s.vy;
s.life--;
});
state.sparks = state.sparks.filter(s => s.life > 0);
if (state.shake > 0) state.shake--;
}
function drawFighter(f) {
ctx.save();
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.45)';
ctx.beginPath();
ctx.ellipse(f.x + f.w / 2, FLOOR_Y + 4, 26, 7, 0, 0, Math.PI * 2);
ctx.fill();
// Body
ctx.fillStyle = f.hitstun > 0 ? '#f8fafc' : f.color;
ctx.beginPath();
ctx.roundRect(f.x, f.y, f.w, f.h, 10);
ctx.fill();
// Headband & visor
ctx.fillStyle = '#0f172a';
ctx.fillRect(f.x + (f.facing === 1 ? 18 : 6), f.y + 14, 22, 10);
// Block Shield
if (f.blocking) {
ctx.strokeStyle = '#38bdf8';
ctx.lineWidth = 4;
ctx.strokeRect(f.x - 6, f.y - 4, f.w + 12, f.h + 8);
}
// Attack Hitbox Graphic
if (f.atk) {
const hx = f.facing === 1 ? f.x + f.w : f.x - f.atk.reach;
ctx.fillStyle = f.atk.type === 'super' ? '#facc15' : (f.atk.type === 'heavy' ? '#fb923c' : '#e2e8f0');
ctx.beginPath();
ctx.roundRect(hx, f.y + 22, f.atk.reach, f.atk.type === 'super' ? 36 : 22, 8);
ctx.fill();
}
ctx.restore();
}
function drawHUD() {
// P1 Health Bar
ctx.fillStyle = '#1e293b';
ctx.fillRect(30, 22, 340, 24);
ctx.fillStyle = '#facc15';
ctx.fillRect(30, 22, 3.4 * state.p1.displayHp, 24);
ctx.fillStyle = '#10b981';
ctx.fillRect(30, 22, 3.4 * state.p1.hp, 24);
// P2 Health Bar
ctx.fillStyle = '#1e293b';
ctx.fillRect(510, 22, 340, 24);
ctx.fillStyle = '#facc15';
ctx.fillRect(510 + (340 - 3.4 * state.p2.displayHp), 22, 3.4 * state.p2.displayHp, 24);
ctx.fillStyle = '#f43f5e';
ctx.fillRect(510 + (340 - 3.4 * state.p2.hp), 22, 3.4 * state.p2.hp, 24);
// Super Meters
ctx.fillStyle = '#1e293b';
ctx.fillRect(30, 52, 220, 10);
ctx.fillStyle = state.p1.meter >= 100 ? '#facc15' : '#38bdf8';
ctx.fillRect(30, 52, 2.2 * state.p1.meter, 10);
ctx.fillStyle = '#1e293b';
ctx.fillRect(630, 52, 220, 10);
ctx.fillStyle = state.p2.meter >= 100 ? '#facc15' : '#ec4899';
ctx.fillRect(630 + (220 - 2.2 * state.p2.meter), 52, 2.2 * state.p2.meter, 10);
// Timer Box
ctx.fillStyle = '#0f172a';
ctx.strokeStyle = '#475569';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(405, 14, 70, 48, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#f8fafc';
ctx.font = 'bold 26px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(state.timer, 440, 47);
if (state.p1.combo > 1) {
ctx.fillStyle = '#facc15';
ctx.font = 'bold 22px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`${state.p1.combo} HIT COMBO!`, 32, 95);
}
}
function render() {
ctx.save();
if (state.shake > 0) {
ctx.translate((Math.random() - 0.5) * state.shake, (Math.random() - 0.5) * state.shake);
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Dojo Floor
ctx.fillStyle = '#1e293b';
ctx.fillRect(0, FLOOR_Y, canvas.width, canvas.height - FLOOR_Y);
ctx.strokeStyle = '#f43f5e';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, FLOOR_Y);
ctx.lineTo(canvas.width, FLOOR_Y);
ctx.stroke();
drawFighter(state.p1);
drawFighter(state.p2);
state.sparks.forEach(s => {
ctx.fillStyle = s.color;
ctx.fillRect(s.x, s.y, 5, 5);
});
drawHUD();
if (state.over) {
ctx.fillStyle = 'rgba(9, 13, 22, 0.78)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#facc15';
ctx.font = 'bold 42px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(state.banner, canvas.width / 2, 215);
ctx.fillStyle = '#e2e8f0';
ctx.font = '18px sans-serif';
ctx.fillText('Press SPACE to Rematch', canvas.width / 2, 260);
}
ctx.restore();
requestAnimationFrame(() => {
update();
render();
});
}
initMatch();
render();
</script>
</body>
</html>
How to Debug Without Losing Your Vibe
Fighting games rely on tight frame logic. Use these three vibe debugging prompts if combat feels off:
One Hit Per Attack Swing
If a single punch drains 100% health over 10 frames, prompt the AI: "Add a boolean hasHit = false to each attack object and set it to true on first collision so a move only damages once."
Corner Pushback Physics
Prevent fighters from trapping each other in infinite corner loops by telling the AI: "If the defender is touching the arena wall when hit, apply the knockback velocity backward onto the attacker instead."
Lock Facing Mid-Swing
Keep jump-ins honest by instructing your AI coding assistant: "Only update a fighter's facing direction when they are idle or walkingβnever flip direction in the middle of an active attack."
The Vibe Prompt Vault
Copy-paste prompts to add 2-player local versus, special move projectiles, and hitbox training mode.
π₯ Prompt: Add Fireball Projectiles & Anti-Air Uppercuts
"Extend my HTML5 Canvas fighting game in index.html with two classic special moves: 1) Pressing 'U' launches a traveling plasma fireball projectile across the stage (costs 25% meter, disappears on hit or stage edge), and 2) Pressing 'I' performs a rising invincible uppercut that launches airborne opponents."
π₯ Prompt: Add 2-Player Local Keyboard & Gamepad Support
"Add a top toggle button to switch my fighting game between '1P vs CPU' and '2P Local Versus'. In 2P mode, let Player 1 use WASD + J/K/L and Player 2 use Arrow Keys + Numpad 1/2/3, and track a Best-of-3 round win indicator under each health bar."
π Prompt: Add a Training Mode Hitbox / Frame Data Overlay
"Add a 'Training Mode' checkbox to my Canvas fighting game that draws translucent green outlines around hurtboxes, bright red boxes around active attack hitboxes, displays total combo damage, and automatically refills health after 2 seconds of no hits."