Skip to content

Musical Critter

Exploration of creating a lively, animated character with sound! (Including breathing noises; hopefully those aren't too disconcerting.)

Use the arrow keys to move around. No mobile support.

A triangular critter in a triangular hoodie stands on a bronze ruler, making sounds as it moves and jumps around.

function setup() {
  createCanvas(400, 400);
  
  characterPanner = new p5.Panner();
  breatheNoise = new p5.Noise('pink')
  breatheNoise.amp(0, 0)
  walkNoise = new p5.Noise('white')
  walkNoise.amp(0, 0)
  jumpNoise = new p5.Noise('brown')
  jumpNoise.amp(0, 0)
  
  breatheNoise.disconnect()
  breatheNoise.connect(characterPanner)
  walkNoise.disconnect()
  walkNoise.connect(characterPanner)
  jumpNoise.disconnect()
  jumpNoise.connect(characterPanner)
  breatheNoise.start()
  walkNoise.start()
  jumpNoise.start()
  
  platformSound = new p5.SinOsc()
  platformSound.start()
  platformSound.amp(0)
  platformEnv = new p5.Envelope()
  platformSound.disconnect()
  platformSound.connect(platformEnv)
  // smooth(1)
  strokeWeight(1.5)
  
  backgroundGradient = createImage(1, 2)
  backgroundGradient.set(0, 0, [200, 230, 240, 200])
  backgroundGradient.set(0, 1, [240, 250, 255, 200])
  backgroundGradient.updatePixels()
}

let characterPanner
let backgroundGradient
let jumpNoise
let walkNoise
let breatheNoise
let platformSound
let platformEnv

let x = 50 // x, y, vx, vy of critter
let y = 300
let vx = 0
let vy = 0
let jumps = 0 // Remaining jumps
let jumping = false // Is jump currently held

let pr = 0 // Platform rotation

let sx = 1 // Walk direction, snapped to -1 or +1
let cx = 0 // Walk cycle, snapped to (2n + 1)* PI
let my = 0 // "Magical" on Y, used for double jump sound
let iy = -1.6 // "Inertia" on Y
let br = 1 // Breathing cycle - position
let vbr = 0 // Breathing cycle - velocity
let act = 0 // Smoothed activity rate, breathing frequency
let act2 = 0 // Smoothed activity rate, breathing amplitude

let volume = 0.3

function draw() {
  image(backgroundGradient, 0, 0, width, height, 0, 0.5, 1, 1)
  
  // Handle movement, exposing acceleration on x and y
  let ax = 1.4 * (keyIsDown(RIGHT_ARROW) - keyIsDown(LEFT_ARROW))
  vx += ax
  vx *= 0.7
  x += vx
  let ay = 1.6
  let ground = 0
  if (y + vy >= 300 && vy >= 0) {
    ground = 1
    jumps = 2
    ay = -vy * 0.75
    y = 300
  } else {
    if (vy < 0 && keyIsDown(UP_ARROW)) {
      ay -= 0.6
    }
  }
  if (keyIsDown(UP_ARROW)) {
    if (!jumping && jumps > 0) {
      jumping = true
      jumps --
      if (!ground) {
        my -= vy + ay + 18
      }
      ay -= vy + ay + 18
      br -= 0.5
    }
  } else {
    jumping = false
  }
  vy += ay
  y += vy
  
  // Handle platform animation
  pr *= -0.5
  pr -= ground * (ay / 3 - 30) * (x/400) * 0.001
  let prc = pr * 1000 // Platform specular color
  fill(250 + prc, 210 + prc, 60 + prc)
  translate(10, 325)
  shearX(0.4)
  rotate(pr)
  rect(-25, -25, 390, 50)
  resetMatrix()
  let force = -(ay) + ground * abs(ax) * 0.1
  // Platform sounds
  if (ground && force > 0.01) {
    platformSound.freq(min(220 * 400 / abs(400 - x), 6000))
    let am = map(abs(400 - x), 0, 400, 0.0, 0.2)
    platformSound.amp((force ** 0.5) * am * volume)
    platformEnv.play()
  }
  
  // Handle breathing
  act += abs(ax) + max(-ay, 0)
  act *= 0.999
  act2 += abs(ax) + max(-ay, 0)
  act2 *= 0.99
  let bramp = 0.3 + act2 / 100 * 0.3 // breathing amplitude
  let bramps = 0.09 + act2 / 100 * 0.08 // breathing amplitude - sound
  let kvbr = 0.002 + act / 1000 * 0.005 // breathing "spring" stiffness
  vbr -= kvbr * br
  br += vbr
  {
    // Restore normal breathing cycle
    // The points (br, vbr * svbr) form an ellipse
    // So we slowly bring them out back to radius ~1
    // (sine waves are so much nicer...)
    let svbr = kvbr ** -0.5
    let dbr = mag(br, vbr * svbr)
    let factor = 1 + (1 / dbr - 1) * 0.01
    br *= factor
    vbr *= factor
  }
  breatheNoise.amp(max((1 - br ** 2) * bramps * volume, 0))
  
  // Handle player's other variables
  let t2 = millis() / 540 // Player head bob
  my *= 0.8 // Smooth "magic" value for sound
  cx += sin(cx + PI) * 0.2 // Snap walk cycle 
  cx += vx * 0.1 // Advance walk cycle 
  sx += vx * 0.3 // Advance direction
  sx += (1 - abs(sx)) * Math.sign(sx) * 0.4 // Snap direction
  sx += (1 - abs(sx)) * Math.sign(sx) * 0.4
  iy = lerp(iy, ay - 1.6, 0.08) // Smoothened acceleration
  
  let sy = (3 + iy / 1.6) ** 0.5 * 0.7 + my * 0.01 // Scaling factor dependent on Y, for offsetting triangles' coordinates
  let cy = 1 + (abs(vy) ** 0.5) + my * 0.01 // Scaling factor dependent on VY, for modifying walk cycle scaling
  
  translate(x, y)
  // Body
  fill(20, 140, 100)
  triangle(
    -18 * sy - br * bramp * 1 + sin(cx - 0.8) * 2,
    25 + cos(cx - 0.8) ** 2 * 2 * cy,
    0,
    -15 + br * bramp  * 1,
    18 * sy + br * bramp  * 1 + sin(cx + 0.8) * 2,
    25 + cos(cx + 0.8) ** 2 * 2 * cy,
  )
  // Hoodie
  fill(200, 50, 50)
  triangle(
    -20 * sy - br * bramp * 2 + sin(cx - 0.4) * 2,
    15 + cos(cx - 0.4) ** 2 * 1 * cy,
    0,
    -25 + br * bramp * 1,
    20 * sy + br * bramp * 2 + sin(cx + 0.4) * 2,
    15 + cos(cx + 0.4) ** 2 * 1 * cy,
  )
  // Face
  fill(0)
  circle(sx * sy * 3 + br * bramp * 1 + sin(t2) * 1, -3 - br * bramp * 0.4, 12)
  fill(255)
  circle(sx * sy * 4 + br * bramp * 1 + sin(t2) * 1, -3 - br * bramp * 0.4, 9)
  // Play sounds
  walkNoise.amp(ground * (1 - cos(cx * 2)) ** 2 * 0.05 * volume, deltaTime/1000)
  jumpNoise.amp(max((constrain(-1.6 - iy, 0, 1) * 0.8 - my * 0.03) * volume, 0), 0)
  
  characterPanner.pan(map(x, 0, 400, -1, 1))
}

function mousePressed() {
  userStartAudio();
}

(Originally seen at https://editor.p5js.org/bojidar-bg/sketches/3caSN606M)

Experiments tagged p5 (70/85)

Experiments tagged critter (5/6)

Experiments tagged interactive (22/26)

Experiments on this site (70/85)