Wrapping preload(), setup() and draw() in an ES6 class

Hi all,

Apologies if this has been discussed before; I did a quick search across Google and this forum but didn’t find much. Please point me in the right direction if this is a duplicate question :slight_smile:

So what I’m trying to do is create a clone of the classic Super Mario Bros. game on the Nintendo Entertainment System, using p5.play.js.

In the interest of adhering to proper OO design principles, I’d like to have the following ES6 class as the main “controller” of my game:

class SuperMarioJS {
    constructor() {
        this.currentLevel = new Level('world 1.1');
        this.mario = new Mario(); // 'Mario' class extends p5.play.Sprite

        // other initialisation code...
    }

    static get GRAVITY() { return 1.0; }

    // other global constants...

    setup() {
        createCanvas(this.currentLevel.width, windowHeight);
        // other setup code...
    }

    draw() {
        this.mario.move();
        drawSprites();
    }
}

And then the only global code attached to the window object would be:

var gameController = new SuperMarioJS();
function setup() { gameController.setup(); }
function draw() { gameController.draw(); }

But this doesn’t work. I don’t get any errors or anything logged to the browser console, but I simply end up with a black screen and nothing else!

So what would be the best way to approach this? MTIA! :smiley:

UPDATE: I just discovered this wiki post about p5 global and instance modes and have updated my code thusly:

class SuperMarioJS {
    static get GRAVITY() { return 1.0; }
    static get SPACEBAR() { return 32; }

    constructor() {
        this.level = new p5(l => {
            l.preload = () => {
                l.bgImage = l.loadImage('assets/maps/1-1.png');
                this.scale = l.windowHeight / l.bgImage.height;

                this.mario = new Mario(l, 48, l.bgImage.height - 40, this.scale);
                this.mario.loadAnimations('mario');

                // blockAnims.push({ 'name': 'item-idle', 'elem': loadAnimation(
                //     loadSpriteSheet('assets/sprites/items/items.png', itemFrames))});
            };

            l.setup = () => {
                l.createCanvas(this.scale * l.bgImage.width, l.windowHeight);

                // ground = new Group();
                // ground.add(new ES6Sprite(width / 2, bgRatio *
                //     (bgImage.height - 31), 1, width, 1));
                // ground.add(new Block(264, 152, bgRatio, ITEM_BLOCK, null));
            };

            l.draw = () => {
                l.image(l.bgImage, 0, 0, l.width, l.height);
                //mario.checkCollisions(ground);
                l.drawSprites();
            };

            l.keyPressed = () => {
                this.mario.onKeyPressed();
            }

            l.keyReleased = () => {
                this.mario.onKeyReleased();
            }
        });
    }
}

let gameController = new SuperMarioJS();

But now I get Error: "createVector" is not a p5 method in the browser console, and the same black screen as before. The problem probably lies in the way I’m extending p5.play.Sprite:

class Mario extends p5.prototype.Sprite {
    constructor(l, x, y, s, w = 16 * s, h = 16 * s) {
        x *= s;
        y *= s;

        super(l, x, y, w, h);
        // other class code...
    }
}

Which was working in global mode, although the l constructor parameter was unnecessary then. In fact, it took me ages to realise that even though p5.play relies on a pInst variable that references the p5 instance, it actually expects that variable to be set to the window object, which it does itself. And because of that, you actually need to omit the pInst parameter from the Sprite constructor!

Now that I understand the difference between global and instance mode, this all finally makes sense…except that I want to run in instance mode, and it still seems to be expecting global mode!

After searching for a solution to this perplexing problem on Google, I came across this GitHub comment posted by @GoToLoop. I tried his/her example code and it works fine, so what the heck is wrong with mine?!

Thanks again for any help that you guys may be able to provide :smiley:

In my original sketch “p5.Play Instance Mode Example”, I rely on p5::createSprite() in order to instantiate class Sprite:

sprite = p.createSprite(p.width>>1, p.height>>1, 0o100, 0o100);

Obviously, it returns an already instantiated Sprite object:
https://github.com/molleindustria/p5.play/blob/master/lib/p5.play.js#L179-L184

But you wanna create a subclass which extends it instead:

However, p5.prototype.Sprite is an extremely hacked version of the original constructor Sprite:
https://github.com/molleindustria/p5.play/blob/master/lib/p5.play.js#L2439-L2440
https://github.com/molleindustria/p5.play/blob/master/lib/p5.play.js#L86-L105
https://github.com/molleindustria/p5.play/blob/master/lib/p5.play.js#L68-L84

And we can’t directly get the original Sprite constructor’s reference b/c it’s inside a factory function:
https://github.com/molleindustria/p5.play/blob/master/lib/p5.play.js#L7-L14
https://github.com/molleindustria/p5.play/blob/master/lib/p5.play.js#L744-L2437

But after analyzing the internal function boundConstructorFactory(), I’ve spotted a glimmer of hope in this statement:
https://github.com/molleindustria/p5.play/blob/master/lib/p5.play.js#L101-L103

The p5.prototype.Sprite that you attempt to inherit from is actually constructor F from boundConstructorFactory().

And F’s prototype{} holds the original constructor.prototype{}'s reference.

And in this case, constructor refers to Sprite.

B/c that constructor.prototype{} object was originally created by the constructor Sprite, its property constructor references back to Sprite:

So, rather than just class Mario extends p5.prototype.Sprite {, which points to a screwed up Sprite constructor, we go w/ class Mario extends p5.prototype.Sprite.prototype.constructor {, which points to the original Sprite:

// class Mario extends p5.prototype.Sprite {
class Mario extends p5.prototype.Sprite.prototype.constructor {
  static get SC() {
    return 16;
  }

  constructor(p, x, y, s = 1, w = Mario.SC * s, h = Mario.SC * s) {
    super(p, x, y, w, h);
    this.depth = p.allSprites.maxDepth() + 1;
    p.allSprites.add(this);
  }
}

Notice that I’ve also added code from method p5::createSprite() inside your fixed subclass Mario’s constructor() after invoking super().

1 Like

Yeah I’ve been doing that too, just didn’t show that part of the code. But thank you so much for such a lengthy and detailed explanation of why boundConstructorFactory() and the hacked prototype constructor were causing me grief!

Your version now works perfectly :smiley: The only thing left to do now is extend the p5 instance into a Level class! :stuck_out_tongue: Any idea how I could do that, using ES6 class syntax? I’ve tried the following:

class Level extends p5 {
    constructor(levelNum) {
        super(() => {
            this.preload = loadLevel;
        });
    }

    loadLevel() {
        // preload() code here...
    }
}

But it doesn’t work :frowning: I also tried extending from p5.prototype.constructor instead of just p5, since that was the solution to my first problem, but in both cases I get ReferenceError: Level is not defined.

Anyway thanks again for the super detailed reply to my initial question :smiley:

Class p5 is the original constructor already. :stuck_out_tongue:
It’s not been hacked like the reference stored in p5.prototype.Sprite. :space_invader:
Here’s a very simple “index.html” code proving so: :nerd_face:


index.html:

<script src=https://cdn.JsDelivr.net/npm/p5></script>
<script src=https://MolleIndustria.GitHub.io/p5.play/lib/p5.play.js></script>

<script>
  console.log('p5 constructor', p5 === p5.prototype.constructor);
  const Sprite = p5.prototype.Sprite;
  console.log('Sprite constructor', Sprite === Sprite.prototype.constructor);
</script>

Here’s my attempt on subclassing p5, based on my previous “Class p5 Extended” sketch: :sunglasses:


index.html:

<script defer src=https://cdn.JsDelivr.net/npm/p5></script>
<script defer src=sketch.js></script>

sketch.js:

/**
 * Class p5 Extended II (v1.1.1)
 * GoToLoop (2019-Jul-31)
 *
 * https://Discourse.Processing.org/t/
 * wrapping-preload-setup-and-draw-in-an-es6-class/13071/5
 *
 * https://Discourse.Processing.org/t/how-i-extends-class-in-p5-js/894/5
 *
 * https://CodePen.io/GoSubRoutine/pen/voJozR/left?editors=0011
 */

'use strict';

class Level extends p5 {
  static get FILENAME() {
    return 'https://upload.Wikimedia.org/wikipedia/commons/thumb/2/2e/' +
           'Processing_3_logo.png/480px-Processing_3_logo.png';
  }

  static redirectP5Callbacks(p) {
    p.preload = p.loadLevel.bind(p); // preload() callback has to be bind()
    p.setup = p.initLevel; // but callback setup() doesn't need bind()
    p.draw = p.runLevel.bind(p); // draw() has to be bind() like preload()
    p.mousePressed = p.mouseDragged = p.restartLevel; // optional for mouse
    p.touchStarted = p.touchMoved = p.restartLevel; // optional for touch
    p.keyPressed = p.keyReleased = p.restartLevel.bind(p); // required for key
  }

  constructor(levelNum) {
    super(Level.redirectP5Callbacks);
    this.levelNum = levelNum;
  }

  loadLevel() {
    this.img = this.loadImage(Level.FILENAME);
  }

  initLevel() {
    this.createCanvas(this.img.width, this.img.height);
    this.noLoop();

    this.rectMode(this.CORNER).ellipseMode(this.CENTER);
    this.colorMode(this.RGB).blendMode(this.BLEND);
    this.fill('yellow').stroke(0).strokeWeight(1.5).strokeCap(this.ROUND);
  }

  runLevel() {
    this.background('#' + this.hex(~~this.random(0x1000), 3));
    this.set(0, 0, this.img);
  }

  restartLevel() {
    this.redraw();
    return false;
  }

  // When bind() isn't needed, we can directly override the callback method:
  windowResized() {
    this.print('LevelNum: ' + this.levelNum);
    return false;
  }
}

// const levels = [ new Level(0), new Level(1) ];
const levels = Array.from({ length: 2 }, (v, i) => new Level(i));

BtW, I’ve hacked the callbacks w/ Function::bind(): :robot:

1 Like

Wow I can’t believe that I was sooooo close to figuring it out myself! Thank you so much @GoToLoop!!

For completeness’ sake, just letting you know that not all p5 callbacks are required to use bind() in order to have the correct this inside their function body.

For example, neither setup() nor mouse and touch related callbacks, even windowResized(), need bind() at all.

However, preload(), draw() and key related callbacks all have a wrong this and require bind().

For those callbacks w/ an already correct this, they can be directly overridden inside the subclass.

For example, we could delete p.setup = p.initLevel; and have method initLevel() renamed as setup() if we wanted to.

Here’s an example I have which goes w/ the p5js instance mode, but uses this instead of parameter p:

Notice above I had to invoke }.bind(p); right after the block function () { in order to assign it to p.draw so the keyword this would correctly refer to the sketch’s p5 current instance; but no bind() was needed for p.setup.

BtW, besides p.draw = p.runLevel.bind(p); syntax, we coulda used p.draw = Level.prototype.runLevel.bind(p); and even p.draw = p.__proto__.runLevel.bind(p); instead.

Also, I’ve posted the new “Class p5 Extended II” version on CodePen.io, so we can run it online too:

https://CodePen.io/GoSubRoutine/pen/voJozR/left?editors=0011

1 Like

Awesome good to know! Thanks again :smiley: