Skip to content

X-Y Mover

A game where you have to navigate an obstacle course using separate X-axis and Y-axis motion devices connected by a wire. An experiment in organizing things with classes, as I ponder how to best explain those to students.

Move with the arrow keys. On mobile, press the side of the screen you want to move in.

An obstacle course with a stick with green and red ends in the top corner and a goal area in the lower right

let scene
let touchesShown = false
function setup() {
  createCanvas(400, 400);
  let a, b
  scene = new Scene([
    new Boundaries(400, 400),
    new Rect(200, 95, 50, 200),
    new Rect(200, 315, 50, 200),
    new Rect(110, 200, 50, 150),
    new Rect(200, 185, 70, 20),
    new Rect(200, 215, 70, 20),
  ], [
    a = new PlayerXY(10, 100, 4, new KeyAxis(new KeyOrTouch(RIGHT_ARROW, [3/4, 1, 0, 1]), new KeyOrTouch(LEFT_ARROW, [0, 1/4, 0, 1])), new ConstAxis(0)),
    b = new PlayerXY(10, 110, 4, new ConstAxis(0), new KeyAxis(new KeyOrTouch(DOWN_ARROW, [0, 1, 3/4, 1]), new KeyOrTouch(UP_ARROW, [0, 1, 0, 1/4]))),
    new PlayerSpacer(100, a, b),
  ], [
    new Goal(0, 315, 315, 150, 150),
    new Goal(1, 200, 370, 120, 30),
    new Goal(2, 200, 30, 120, 30),
    new Goal(3, 85, 85, 150, 150),
    new WinText(4, 100)
  ])
  noStroke()
  noFill()
}

function draw() {
  background(170);
  scene.update()
  scene.draw()
}

const SOLVER_ITERATIONS = 3

class Scene {
  constructor(objects, players, pickups) {
    this.objects = new Set(objects)
    this.players = players
    this.pickups = new Set(pickups)
    this.levelStep = 0
  }
  update() {
    for (let player of this.players) {
      player.input(this)
    }
    for (let i = 0; i < SOLVER_ITERATIONS; i++) {
      for (let player of this.players) {
        player.update(this)
      }
    }
    for (let pickup of this.pickups) {
      pickup.update(this)
    }
  }
  draw() {
    for (let object of this.objects) {
      push()
      object.draw(this)
      pop()
    }
    for (let player of this.players) {
      push()
      player.draw(this)
      pop()
    }
    for (let pickup of this.pickups) {
      push()
      pickup.draw(this)
      pop()
    }
  }
}

const REPEL_MARGIN = 10

class Rect {
  constructor(x, y, w, h) {
    Object.assign(this, {x, y, w, h})
    this.rw = this.w + REPEL_MARGIN
    this.rh = this.h + REPEL_MARGIN
  }
  draw() {
    fill(0)
    rectMode(CENTER)
    rect(this.x, this.y, this.w, this.h)
  }
  repel({x, y}) {
    let dx = ((x - this.x) / this.rw)
    let dy = ((y - this.y) / this.rh)
    let collision = abs(dx) < 0.5 && abs(dy) < 0.5
    let collisionOnX = abs(dx) + random(-0.01, 0.01) > abs(dy)
    return {
      x: collision && collisionOnX ? this.x + Math.sign(dx) * this.rw / 2 : x,
      y: collision && !collisionOnX ? this.y + Math.sign(dy) * this.rh / 2 : y
    }
  }
}

class Circle {
  constructor(x, y, d) {
    Object.assign(this, {x, y, d})
    this.rr = (this.d + REPEL_MARGIN) / 2
  }
  draw() {
    fill(0)
    rectMode(CENTER)
    circle(this.x, this.y, this.d)
  }
  repel({x, y}) {
    let d = dist(x, y, this.x, this.y)
    return {
      x: d < this.rr ? this.x + (x - this.x) / d * this.rr : x,
      y: y < this.rr ? this.y + (y - this.y) / d * this.rr : y
    }
  }
}

class Boundaries {
  constructor(w, h) {
    Object.assign(this, {w, h})
  }
  draw() {}
  repel({x, y}) {
    return {
      x: constrain(x, REPEL_MARGIN/2, this.w - REPEL_MARGIN/2),
      y: constrain(y, REPEL_MARGIN/2, this.h - REPEL_MARGIN/2)
    }
  }
}

class Pickup {
  constructor(x, y, w, h) {
    Object.assign(this, {x, y, w, h})
  }
  draw(scene) {
    rectMode(CENTER)
    rect(this.x, this.y, this.w, this.h)
  }
  update(scene) {
    for (let player of scene.players) {
      let dx = (player.x - this.x) / this.w
      let dy = (player.y - this.y) / this.h
      if (abs(dx) > 0.5 || abs(dy) > 0.5) {
        return
      }
    }
    this._take(scene)
  }
  _take(scene) {
  }
}

class WinText { // implements Pickup
  constructor(step, framesShown = -1) {
    Object.assign(this, {step, framesShown})
  }
  draw(scene) {
    if (scene.levelStep == this.step) {
      fill(255)
      noStroke()
      textSize(80)
      textAlign(CENTER, CENTER)
      text('🎉', width / 2, height / 2)
    }
  }
  update(scene) {
    if (scene.levelStep == this.step) {
      if (this.framesShown == 0) {
        scene.levelStep ++
      }
      this.framesShown --
    }
  }
}

class Goal extends Pickup {
  constructor(step, ...args) {
    super(...args)
    Object.assign(this, {step})
  }
  draw(scene) {
    if (scene.levelStep == this.step) {
      fill(155, 155, 0, 40)
      stroke(100, 100, 0)
      super.draw()
    }
  }
  _take(scene) {
    if (scene.levelStep == this.step) {
      scene.levelStep ++
    }
  }
}

class ConstAxis {
  constructor(value = 0) {
    Object.assign(this, {value, color: 0})
  }
  draw() {
  }
}
class KeyAxis {
  constructor(keyPos, keyNeg) {
    Object.assign(this, {keyPos, keyNeg, color: 255})
  }
  get value() {
    return this.keyPos.pressed - this.keyNeg.pressed
  }
  draw() {
    this.keyPos.draw()
    this.keyNeg.draw()
  }
}
class KeyOrTouch {
  constructor(key, touchBox = [0, 0, 0, 0]) {
    Object.assign(this, {key, touchBox, shown: false})
  }
  get pressed() {
    let pressed = keyIsDown(this.key)
    for (let touch of touches) {
      let [rx, ry] = [touch.x / width, touch.y / height]
      if (rx > this.touchBox[0] && rx < this.touchBox[1] && ry > this.touchBox[2] && ry < this.touchBox[3]) {
        pressed = true
      }
      this.shown = true
    }
    return pressed
  }
  draw() {
    if (this.shown) {
      noFill()
      blendMode(ADD)
      rect(this.touchBox[0] * width + 1, this.touchBox[2] * height + 1, this.touchBox[1] * width - 1, this.touchBox[3] * height - 1)
      blendMode(BLEND)
    }
  }
}

class PlayerXY {
  constructor(x, y, speed, axisX, axisY) {
    Object.assign(this, {x, y, speed, axisX, axisY})
  }
  input() {  
    this.x += this.speed * this.axisX.value
    this.y += this.speed * this.axisY.value
  }
  update(scene) {
    let {x, y} = this
    for (let object of scene.objects) {
      ({x, y} = object.repel({x, y}))
    }
    Object.assign(this, {x, y})
  }
  draw() {
    fill(this.axisX.color, this.axisY.color, 0)
    circle(this.x, this.y, REPEL_MARGIN)
    stroke(this.axisX.color, this.axisY.color, 0)
    this.axisX.draw()
    this.axisY.draw()
  }
}
class PlayerSpacer {
  constructor(distance, player1, player2) {
    Object.assign(this, {
      distance,
      players: [player1, player2],
    })
    this.x = (this.players[0].x + this.players[1].x) / 2
    this.y = (this.players[0].y + this.players[1].y) / 2
    this.ox = this.x
    this.oy = this.y
  }
  input() {}
  update(scene) {
    let mx = this.x - this.ox
    let my = this.y - this.oy
    this.x = (this.players[0].x + this.players[1].x) / 2 + mx
    this.y = (this.players[0].y + this.players[1].y) / 2 + my
    this.ox = this.x
    this.oy = this.y
    
    let currentDistance = dist(this.players[0].x, this.players[0].y, this.players[1].x, this.players[1].y)
    let dx = (this.players[0].x - this.players[1].x) / currentDistance * this.distance
    let dy = (this.players[0].y - this.players[1].y) / currentDistance * this.distance
    this.players[0].x = this.x + dx / 2
    this.players[1].x = this.x - dx / 2
    this.players[0].y = this.y + dy / 2
    this.players[1].y = this.y - dy / 2
  }
  draw() {
    blendMode(DIFFERENCE)
    stroke(255)
    if (this.players) {
      line(this.players[0].x, this.players[0].y, this.players[1].x, this.players[1].y)
    }
  }
}

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

Experiments tagged p5 (68/85)

Experiments tagged game (10/11)

Experiments on this site (68/85)