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.
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)
Browse more articles?
← 3D Maze Experiments tagged p5 (68/85) Copper melange →
← 3D Maze Experiments tagged game (10/11) Cursor survival →
← 3D Maze Experiments on this site (68/85) Copper melange →