Sketch 2026-04-30 12:48

This is a Jujutsu Kaisen-inspired fighting game where two cursed spirits battle in an arena. Players control characters with health bars, special moves, and collision-based combat mechanics across multiple themed battlefields.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make players bigger targets
  2. Double the collision hit box
  3. Make jumps higher and floatier
  4. Reduce damage per hit
  5. Slow down gravity
  6. Make particles glow longer
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a competitive fighting game inspired by Jujutsu Kaisen, where two cursed spirits clash in destructible arenas. The visuals combine geometric shapes, color gradients, particle effects, and real-time health bar updates to create an intense arcade aesthetic. It demonstrates core p5.js techniques including collision detection, frame-based animation, object-oriented design with character classes, interactive UI elements, and state management across multiple screens.

The code is organized around a central `Player` class that handles character movement, combat, and visual rendering, with a main game loop that updates physics, checks collisions, and manages screen transitions. By studying this sketch, you will learn how to structure a complete game in p5.js: creating reusable objects, implementing hit detection, managing game states (menu, arena select, gameplay, victory), and coordinating visual feedback like health bars and particle bursts that react to player actions.

⚙️ How It Works

  1. When the sketch loads, setup() creates the canvas and displays the main menu. Players click 'ENTER THE VEIL' to see stage options, then select a battlefield which sets the arena background color and initializes two Player objects facing each other.
  2. Every frame, draw() calls updateGame(), which moves both characters based on keyboard input (WASD for P1, arrow keys for P2) and applies gravity to simulate falling.
  3. Collision detection checks if either player touches the arena walls (bouncing them back), ground (stopping vertical movement), or the opponent (triggering hit effects).
  4. When players collide, takeDamage() reduces the opponent's health, creates a red flash effect, and spawns particles at the impact point to visualize the hit.
  5. Health bars update in real-time using HTML elements; when a player's HP reaches zero, the game switches to a victory screen showing the winner's name and an inspirational quote.
  6. Players can trigger special 'Domain Expansion' moves by pressing spacebar, which displays a fullscreen cutscene and resets the round with dramatic effect.

🎓 Concepts You'll Learn

Object-oriented programming with ES6 classesCollision detection and physics responseGame state managementKeyboard input and player controlHealth system and UI updatesParticle effects and visual feedbackHTML/CSS integration with p5.jsAnimation and sprite-like rendering

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It prepares the canvas and game settings before any gameplay begins.

function setup() {
  createCanvas(950, 600);
  frameRate(60);
}
Line-by-line explanation (2 lines)
createCanvas(950, 600);
Creates a 950×600 pixel canvas that serves as the game's display area
frameRate(60);
Sets the sketch to run at 60 frames per second, creating smooth animation

draw()

draw() is the main loop running 60 times per second. It separates game logic (updateGame) from visuals (displayGame), which is a professional game programming pattern.

function draw() {
  if (gameActive) {
    updateGame();
    displayGame();
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Game Active Check if (gameActive) {

Only runs game logic when gameplay is active—pauses during menus and cutscenes

if (gameActive) {
Checks if the game is currently running (not on a menu or cutscene screen)
updateGame();
Calls the function that handles all game logic: movement, gravity, collision detection, and damage
displayGame();
Calls the function that renders everything: the background, both players, and particle effects

updateGame()

updateGame() is the heart of the game loop—it handles input reading, physics calculations, collision detection, and win conditions. This is where the game's 'rules' live.

function updateGame() {
  // Player 1 controls (WASD)
  if (key === 'w' && p1.onGround) p1.vy = -jumpPower;
  if (key === 'a') p1.vx = -moveSpeed;
  if (key === 'd') p1.vx = moveSpeed;
  if (key === ' ') p1.vy = -jumpPower;
  
  // Player 2 controls (Arrow keys)
  if (keyIsDown(UP_ARROW) && p2.onGround) p2.vy = -jumpPower;
  if (keyIsDown(LEFT_ARROW)) p2.vx = -moveSpeed;
  if (keyIsDown(RIGHT_ARROW)) p2.vx = moveSpeed;
  
  // Apply gravity
  p1.vy += gravity;
  p2.vy += gravity;
  
  // Update positions
  p1.x += p1.vx;
  p1.y += p1.vy;
  p2.x += p2.vx;
  p2.y += p2.vy;
  
  // Reset ground state
  p1.onGround = false;
  p2.onGround = false;
  
  // Boundary collisions
  if (p1.x < 25) p1.x = 25;
  if (p1.x > width - 25) p1.x = width - 25;
  if (p1.y > height - 50) {
    p1.y = height - 50;
    p1.onGround = true;
    p1.vy = 0;
  }
  
  if (p2.x < 25) p2.x = 25;
  if (p2.x > width - 25) p2.x = width - 25;
  if (p2.y > height - 50) {
    p2.y = height - 50;
    p2.onGround = true;
    p2.vy = 0;
  }
  
  // Collision detection between players
  let d = dist(p1.x, p1.y, p2.x, p2.y);
  if (d < 50 && !p1.hitCooldown) {
    p2.takeDamage(damage, p1.x);
    p1.hitCooldown = 10;
    createParticles(p2.x, p2.y, 8);
  }
  if (d < 50 && !p2.hitCooldown) {
    p1.takeDamage(damage, p2.x);
    p2.hitCooldown = 10;
    createParticles(p1.x, p1.y, 8);
  }
  
  // Cooldown timer
  if (p1.hitCooldown > 0) p1.hitCooldown--;
  if (p2.hitCooldown > 0) p2.hitCooldown--;
  
  // Update particles
  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update();
    if (particles[i].life <= 0) particles.splice(i, 1);
  }
  
  // Victory condition
  if (p1.hp <= 0) {
    declareWinner('P2');
    gameActive = false;
  }
  if (p2.hp <= 0) {
    declareWinner('P1');
    gameActive = false;
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Player 1 Input Handler if (key === 'w' && p1.onGround) p1.vy = -jumpPower;

Detects WASD keys and updates Player 1's velocity for movement and jumping

conditional Player 2 Input Handler if (keyIsDown(UP_ARROW) && p2.onGround) p2.vy = -jumpPower;

Detects arrow keys and updates Player 2's velocity for movement and jumping

calculation Gravity Application p1.vy += gravity; p2.vy += gravity;

Constantly pulls both players downward each frame, simulating gravity

calculation Position Update p1.x += p1.vx; p1.y += p1.vy;

Moves each player by adding their velocity to their current position

conditional Boundary Collision Detection if (p1.x < 25) p1.x = 25;

Prevents players from leaving the arena by clamping their x and y coordinates

conditional Player-to-Player Collision if (d < 50 && !p1.hitCooldown) {

Detects when two players touch and triggers damage with hit cooldown to prevent instant repeat hits

conditional Victory Condition if (p1.hp <= 0) {

Checks if either player's HP reaches zero and ends the game

if (key === 'w' && p1.onGround) p1.vy = -jumpPower;
When 'w' is pressed and P1 is touching ground, set vertical velocity to negative jumpPower to make them jump upward
if (key === 'a') p1.vx = -moveSpeed;
When 'a' is pressed, set P1's horizontal velocity to negative moveSpeed, moving them left
p1.vy += gravity;
Every frame, increase P1's downward velocity slightly—this creates the illusion of falling
p1.x += p1.vx;
Move P1 horizontally by adding their velocity to their x position
p1.onGround = false;
Reset the ground state each frame; it will be set to true only if they collide with the ground
if (p1.x < 25) p1.x = 25;
If P1 moves too far left, snap them back to x=25 (arena wall boundary)
if (p1.y > height - 50) {
If P1 falls below ground level (height - 50), place them on the ground and stop their downward movement
let d = dist(p1.x, p1.y, p2.x, p2.y);
Calculate the distance between the two players using p5.js's dist() function
if (d < 50 && !p1.hitCooldown) {
If distance is less than 50 pixels (collision) AND P1 hasn't hit P2 recently, trigger damage on P2
p1.hitCooldown = 10;
Set a 10-frame cooldown so the same collision doesn't deal damage multiple times per second
if (p1.hp <= 0) {
If P1's health reaches zero, declare P2 as the winner and stop the game

displayGame()

displayGame() separates rendering from logic—it only draws what's already been calculated. This makes the code easier to debug and modify.

function displayGame() {
  background(arenaColor);
  
  // Draw arena border
  stroke(255);
  strokeWeight(4);
  noFill();
  rect(10, 10, width - 20, height - 20);
  
  // Draw players
  p1.display();
  p2.display();
  
  // Draw particles
  for (let p of particles) {
    p.display();
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Background Rendering background(arenaColor);

Fills the entire canvas with the selected arena's color each frame

calculation Arena Border rect(10, 10, width - 20, height - 20);

Draws a white rectangle outline that frames the playing area

calculation Player Rendering p1.display();

Calls each player's display() method to draw them at their current position

for-loop Particle Rendering Loop for (let p of particles) {

Loops through all active particles and draws each one

background(arenaColor);
Fills the canvas with the current arena's background color, erasing the previous frame
stroke(255);
Sets the outline color to white for all subsequent shapes
strokeWeight(4);
Makes the border outline 4 pixels thick
noFill();
Tells the next shape to have no fill, only an outline
rect(10, 10, width - 20, height - 20);
Draws a rectangle 10 pixels from each edge, creating the arena's visible border
p1.display();
Calls Player 1's display method to draw them as a colored circle at their current x,y position
p2.display();
Calls Player 2's display method to draw them at their position
for (let p of particles) {
Loops through the particles array, giving each particle a chance to draw itself
p.display();
Calls the particle's display method to draw it as a small fading shape

createParticles(x, y, count)

createParticles() demonstrates procedural animation—instead of hand-animating each particle, math (trigonometry with sin/cos) automatically spreads them in all directions. This is how game engines create explosion effects.

function createParticles(x, y, count) {
  for (let i = 0; i < count; i++) {
    let angle = random(TWO_PI);
    let speed = random(2, 6);
    let vx = cos(angle) * speed;
    let vy = sin(angle) * speed;
    particles.push(new Particle(x, y, vx, vy));
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Particle Creation Loop for (let i = 0; i < count; i++) {

Repeats the particle spawning logic 'count' times to create a burst of particles

calculation Random Direction Calculation let angle = random(TWO_PI);

Chooses a random angle between 0 and 360 degrees for each particle

calculation Velocity Vector Calculation let vx = cos(angle) * speed;

Converts the random angle into x and y velocity components using trigonometry

function createParticles(x, y, count) {
Defines a function that takes the spawn position (x, y) and how many particles to create (count)
for (let i = 0; i < count; i++) {
Loops 'count' times (e.g., 8 times), each iteration creates one new particle
let angle = random(TWO_PI);
Chooses a random angle from 0 to 360 degrees—this is why particles burst in all directions
let speed = random(2, 6);
Each particle gets a random speed between 2 and 6 pixels per frame so they don't all move identically
let vx = cos(angle) * speed;
Uses cosine to convert the angle into a horizontal velocity component
let vy = sin(angle) * speed;
Uses sine to convert the angle into a vertical velocity component—together these create diagonal motion
particles.push(new Particle(x, y, vx, vy));
Creates a new Particle object and adds it to the particles array so it will be updated and drawn each frame

initArena(color, name)

initArena() is called when a player selects a stage. It resets the game state, spawns fresh players, and toggles HTML elements to show/hide the right screens at the right time.

function initArena(color, name) {
  arenaColor = color;
  p1 = new Player(100, height / 2, '#ff0000');
  p2 = new Player(width - 100, height / 2, '#0000ff');
  gameActive = true;
  particles = [];
  document.getElementById('menu').style.display = 'none';
  document.getElementById('stage-select').style.display = 'none';
  document.getElementById('ui').style.display = 'flex';
  document.getElementById('p1-tag').textContent = 'P1 ' + name;
  document.getElementById('p2-tag').textContent = 'P2 ' + name;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Player Spawning p1 = new Player(100, height / 2, '#ff0000');

Creates Player 1 at the left side of the arena with red color

calculation UI State Toggle document.getElementById('ui').style.display = 'flex';

Shows the health bars and hides the stage select menu

arenaColor = color;
Sets the global arenaColor variable to the selected battlefield's color
p1 = new Player(100, height / 2, '#ff0000');
Creates Player 1 as a new Player object positioned 100 pixels from the left, centered vertically, with red color
p2 = new Player(width - 100, height / 2, '#0000ff');
Creates Player 2 on the right side (width - 100) with blue color so they start facing each other
gameActive = true;
Sets gameActive to true, enabling the game loop to start running
particles = [];
Clears the particles array so leftover particles from previous rounds don't appear
document.getElementById('ui').style.display = 'flex';
Shows the health bar UI by changing its CSS display from 'none' to 'flex'

declareWinner(player)

declareWinner() demonstrates HTML integration—p5.js sketches can manipulate web page content using JavaScript's DOM methods to create seamless UI experiences.

function declareWinner(player) {
  let quotes = {
    'P1': '"The strongest sorcerer..."',
    'P2': '"I am inevitable..."'
  };
  document.getElementById('win-text').textContent = player + ' HAS PREVAILED';
  document.getElementById('quote').textContent = quotes[player] || '"Victory."';
  document.getElementById('win-screen').style.display = 'flex';
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Quote Selection let quotes = {

Stores different victory quotes for each player using an object

let quotes = {
Creates an object that maps player names to their victory quote strings
document.getElementById('win-text').textContent = player + ' HAS PREVAILED';
Updates the HTML to display which player won (e.g., 'P1 HAS PREVAILED')
document.getElementById('quote').textContent = quotes[player] || '"Victory."';
Displays the winning player's quote, or a default quote if the player isn't found
document.getElementById('win-screen').style.display = 'flex';
Shows the victory screen by changing its CSS from 'none' to 'flex'

showStages()

showStages() is a simple screen transition function—it's called when 'ENTER THE VEIL' is clicked on the main menu.

function showStages() {
  document.getElementById('menu').style.display = 'none';
  document.getElementById('stage-select').style.display = 'flex';
}
Line-by-line explanation (2 lines)
document.getElementById('menu').style.display = 'none';
Hides the main menu by setting its CSS display to 'none'
document.getElementById('stage-select').style.display = 'flex';
Shows the stage selection screen by setting its CSS display to 'flex'

Player class

The Player and Particle classes demonstrate object-oriented design in p5.js. Each object encapsulates its own data (x, y, hp) and methods (display, takeDamage), making the code modular and reusable. The Particle class shows how small effects compound to create visual polish.

class Player {
  constructor(x, y, color) {
    this.x = x;
    this.y = y;
    this.vx = 0;
    this.vy = 0;
    this.color = color;
    this.hp = maxHealth;
    this.maxHealth = maxHealth;
    this.onGround = false;
    this.hitCooldown = 0;
    this.size = 30;
  }
  
  takeDamage(amount, hitFrom) {
    this.hp -= amount;
    this.flashTime = 10; // Flash effect duration
  }
  
  display() {
    if (this.flashTime > 0) {
      fill(255, 100, 100); // Light red flash
      this.flashTime--;
    } else {
      fill(this.color);
    }
    circle(this.x, this.y, this.size);
    
    // Draw outline
    stroke(this.color);
    strokeWeight(2);
    noFill();
    circle(this.x, this.y, this.size);
    
    // Update HP bar in HTML
    let hpPercent = (this.hp / this.maxHealth) * 100;
    if (this.color === '#ff0000') {
      document.getElementById('p1-hp').style.width = hpPercent + '%';
    } else {
      document.getElementById('p2-hp').style.width = hpPercent + '%';
    }
  }
}

class Particle {
  constructor(x, y, vx, vy) {
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
    this.life = 30; // frames until death
    this.maxLife = 30;
  }
  
  update() {
    this.x += this.vx;
    this.y += this.vy;
    this.vy += 0.2; // gravity on particles
    this.life--;
  }
  
  display() {
    let alpha = (this.life / this.maxLife) * 255; // Fade out
    fill(255, 0, 0, alpha);
    noStroke();
    circle(this.x, this.y, 5);
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

calculation Player Constructor constructor(x, y, color) {

Initializes a new Player with position, color, health, and movement properties

calculation Take Damage Method takeDamage(amount, hitFrom) {

Reduces HP and triggers a visual flash effect when hit

conditional Player Display Method if (this.flashTime > 0) {

Draws the player as a circle, with a red flash if recently hit, and updates the HTML health bar

calculation Particle Constructor constructor(x, y, vx, vy) {

Creates a small projectile with position, velocity, and a lifespan

calculation Particle Update Method update() {

Moves the particle and applies gravity, counting down its remaining lifespan

class Player {
Defines the Player class—a blueprint for creating player objects with properties and methods
constructor(x, y, color) {
The constructor function runs when a new Player is created, setting up initial values
this.hp = maxHealth;
Sets the player's starting HP to the global maxHealth constant
this.onGround = false;
Initially sets the player as not on ground (used to check if they can jump)
this.hp -= amount;
Reduces health by the damage amount when takeDamage() is called
this.flashTime = 10;
Triggers a 10-frame red flash visual to show the player was just hit
if (this.flashTime > 0) {
If flashTime is still counting down, draw the player in light red to show the hit effect
fill(this.color);
Otherwise, fill the player circle with their normal color (red or blue)
circle(this.x, this.y, this.size);
Draws the player as a circle at their current position with their size
let hpPercent = (this.hp / this.maxHealth) * 100;
Calculates what percentage of health remains (0-100) to update the HTML health bar width
document.getElementById('p1-hp').style.width = hpPercent + '%';
Updates the P1 health bar element's width to match the HP percentage
class Particle {
Defines the Particle class—a blueprint for small projectiles created by hit bursts
this.life = 30;
Each particle lasts for 30 frames before disappearing
this.x += this.vx;
Moves the particle by its velocity each frame
this.vy += 0.2;
Applies downward gravity to particles so they arc and fall realistically
let alpha = (this.life / this.maxLife) * 255;
Calculates transparency that decreases as life counts down, creating a fade-out effect
fill(255, 0, 0, alpha);
Fills particles with red color with transparency based on remaining life

📦 Key Variables

gameActive boolean

Tracks whether the game is currently running or paused on a menu/cutscene screen

let gameActive = false;
arenaColor string

Stores the current battleground's background color code (e.g., '#050508' for Shibuya)

let arenaColor = '#000000';
p1 object

Reference to Player 1's object, containing their position, health, velocity, and methods

let p1;
p2 object

Reference to Player 2's object, containing their position, health, velocity, and methods

let p2;
particles array

Array storing all active Particle objects that are currently being displayed and updated

let particles = [];
maxHealth number

Constant defining how many HP points each player starts with

const maxHealth = 100;
moveSpeed number

Constant controlling how fast players move when pressing movement keys (pixels per frame)

const moveSpeed = 4;
gravity number

Constant controlling how quickly players and particles fall downward (pixels per frame squared)

const gravity = 0.4;
jumpPower number

Constant defining how much upward velocity is applied when a player jumps

const jumpPower = 15;
damage number

Constant controlling how much health is removed from a player when hit by the opponent

const damage = 15;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG updateGame() collision detection

Collision damage can apply twice per frame to both players simultaneously, causing double damage

💡 Use a shared collision flag or increase hit cooldown to prevent mutual damage in the same frame

PERFORMANCE draw()

No frame rate limiting or vsync—sketch may run differently on high-refresh monitors

💡 The frameRate(60) in setup() helps, but consider using deltaTime for frame-independent movement

STYLE Player and Particle classes

No velocity damping or friction—characters slide indefinitely without friction

💡 Add this.vx *= 0.95 each frame to gradually slow down sliding for more realistic movement

FEATURE Game mechanics

No special moves or ability system beyond movement

💡 Implement spacebar ability (Domain Expansion cutscene exists in HTML but unused) with cooldown and unique effects

BUG updateGame() boundary checking

Players can get stuck at walls if moving too fast—position snaps don't account for velocity

💡 Apply velocity damping or use collision response that bounces players away from walls instead of snapping

STYLE Player.display()

Health bar update happens in display() every frame (tight coupling between rendering and data)

💡 Move HTML updates to a separate function called from updateGame() to separate concerns

🔄 Code Flow

Code flow showing setup, draw, updategame, displaygame, createparticles, initarena, declarewinner, showstages, player

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> gameactive-check[Game Active Check] gameactive-check -->|if active| updategame[updateGame] updategame --> player1-input[Player 1 Input Handler] player1-input --> gravity-application[Gravity Application] gravity-application --> position-update[Position Update] position-update --> boundary-collision[Boundary Collision Detection] boundary-collision --> player-collision[Player-to-Player Collision] player-collision --> victory-check[Victory Condition] victory-check -->|if not won| displaygame[displayGame] displaygame --> background-render[Background Rendering] background-render --> border-draw[Arena Border] border-draw --> player-render[Player Rendering] player-render --> particle-render[Particle Rendering Loop] particle-render --> loop-particle-create[Particle Creation Loop] loop-particle-create --> random-direction[Random Direction Calculation] random-direction --> velocity-calculation[Velocity Vector Calculation] velocity-calculation --> particle-constructor[Particle Constructor] particle-constructor --> particle-update[Particle Update Method] particle-update --> loop-particle-create click setup href "#fn-setup" click draw href "#fn-draw" click gameactive-check href "#sub-gameactive-check" click updategame href "#fn-updategame" click player1-input href "#sub-player1-input" click gravity-application href "#sub-gravity-application" click position-update href "#sub-position-update" click boundary-collision href "#sub-boundary-collision" click player-collision href "#sub-player-collision" click victory-check href "#sub-victory-check" click displaygame href "#fn-displaygame" click background-render href "#sub-background-render" click border-draw href "#sub-border-draw" click player-render href "#sub-player-render" click particle-render href "#sub-particle-render" click loop-particle-create href "#sub-loop-particle-create" click random-direction href "#sub-random-direction" click velocity-calculation href "#sub-velocity-calculation" click particle-constructor href "#sub-particle-constructor" click particle-update href "#sub-particle-update"

Preview

Sketch 2026-04-30 12:48 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-04-30 12:48 - Code flow showing setup, draw, updategame, displaygame, createparticles, initarena, declarewinner, showstages, player
Code Flow Diagram