HEADLESS mode: Uncaught [TypeError: Cannot read property 'gl' of null]

I’m following this tutorial to start building a multiplayer game with express and jsdom. My game runs locally in VSCode’s Live Server. However, when I launch node --inspect server/index.js, I get the following error (from Chrome DevTools):

Uncaught TypeError: Cannot read property 'gl' of null

I’ve tried Phaser versions 3.15.1, 3.24.1, 3.50.0 and 3.50.1, and node versions 15.y.z and 10.13.0 – all give the same error (the tutorial uses 3.15.1 and 10.13.0). I’m on MacOS 10.15.7.

Any idea?

Here’s my simplified code. I realise it’s a lot of code, and I apologise. I really appreciate any hints you can give me.

server/index.js
const path = require('path');
const jsdom = require('jsdom');
const express = require('express');
const app = express();
const server = require('http').Server(app);
const { JSDOM } = jsdom;

app.use(express.static(__dirname + '/public'));

app.get('/', function(req, res) {
    res.sendFile(__dirname + '/index.html');
});

server.listen(8081, function () {
    console.log(`Listening on ${server.address().port}`);
});

function setupAuthoritativePhaser() {
    JSDOM.fromFile(path.join(__dirname, 'authoritative_server/index.html'), {
        runScripts: 'dangerously',
        // ^ allows JSDOM to run scripts in the html file
        resources: 'usable',
        // ^ allows JSDOM to load external resources
        pretendToBeVisual: true,
        // ^ makes JSDOM behave like a visual browser
    });
}

setupAuthoritativePhaser();
server/authoritative_server/index.html
<!doctype html> 
<html lang="en"> 
    <head> 
        <meta charset="UTF-8" />
        <title>Sharing Slime</title>
        <script src="https://cdn.jsdelivr.net/npm/phaser@3.24.1/dist/phaser.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/phaser-matter-collision-plugin/dist/phaser-matter-collision-plugin.js"></script>
        <style type="text/css">
            body { margin: 0; }
        </style>
    </head>
    <body>
        <script src="js/game.js"></script>
    </body>
</html>
server/authoritative_server/js/game.js
const config = {
    type: Phaser.HEADLESS,
    autoFocus: false,
    width: 400,
    height: 240,
    pixelArt: true,
    zoom: 2,
    physics: {
      default: 'matter',
      matter: {
        gravity: {y: 1.5},
      }
    },
    // Add plugins
    plugins: {
      scene: [
        {
          phaser-matter-collision-plugin
          plugin: PhaserMatterCollisionPlugin,
          key: 'matterCollision',
          mapping: 'matterCollision'
        }
      ]
    },
    scene: {
      preload() {
        /* Commented stuff -- still raises error */
      },
      
      create() {
        console.log("Running AUTHORITATIVE");
        /* Commented stuff -- still raises error */
      },

      update() {

      },
    }
  };
  
const game = new Phaser.Game(config);

(The files below run locally in VSCode’s Live Server)

server/public/index.html
<!doctype html> 
<html lang="en"> 
    <head> 
        <meta charset="UTF-8" />
        <title>Sharing Slime</title>
        <script src="https://cdn.jsdelivr.net/npm/phaser@3.24.1/dist/phaser.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/phaser-matter-collision-plugin/dist/phaser-matter-collision-plugin.js"></script>
        <style type="text/css">
            body { margin: 0; }
        </style>
    </head>
    <body>
        <!-- <script src="/socket.io/socket.io.js"></script> -->
        <script type="module" src="js/game.js"></script>
    </body>
</html>
server/public/js/game.js
import Player from "./player.js";

const config = {
    type: Phaser.AUTO,
    parent: 'sharing-slime',
    width: 400,
    height: 240,
    pixelArt: true,
    zoom: 2,
    physics: {
      default: 'matter',
      matter: {
        gravity: {y: 1.5},
        // debug: true,
      }
    },
    // Add plugins
    plugins: {
      scene: [
        {
          plugin: PhaserMatterCollisionPlugin,
          key: 'matterCollision',
          mapping: 'matterCollision'
        }
      ]
    },
    scene: {
      preload() {
        // Load tileset in PNG and JSON form
        this.load.image('base_tiles', 'assets/base_tiles.png');
        this.load.tilemapTiledJSON('tilemap', 'assets/v1.2.2/tilemap.json');
        // Load players' sprite sheet
        this.load.spritesheet('red', 'assets/red.png', {
          frameWidth: 32,
          frameHeight: 32,
        });
        this.load.spritesheet('blue', 'assets/blue.png', {
          frameWidth: 32,
          frameHeight: 32,
        });
        // Load other sprites
        this.load.image('bomb', 'assets/bomb.png');
      },
      
      create() {
        // this.socket = io();
        console.log("Running PUBLIC");

        this.keys = this.input.keyboard.createCursorKeys();
        this.matter.world.setBounds(0, 0, game.config.width, game.config.height, 200);

        // Create map
        const map = this.make.tilemap({key: 'tilemap'});
        // Add the tileset image we are using
        const tileset = map.addTilesetImage('standard_tiles', 'base_tiles', 16, 16, 2, 2);
        // Create remaining layer, in order
        const skyLayer = map.createDynamicLayer('Sky', tileset, 0, 0);
        const platformLayer = map.createDynamicLayer('Platform', tileset, 0, 0);
        const groundLayer = map.createDynamicLayer('Ground', tileset, 0, 0);

        // Enable collisions with layers
        groundLayer.setCollisionByProperty({collides: true});
        platformLayer.setCollisionByProperty({collides: true});
        this.matter.world.convertTilemapLayer(groundLayer);
        this.matter.world.convertTilemapLayer(platformLayer);
        // Visualise all the matter bodies
        // this.matter.world.createDebugGraphic();
        
        // Create player 1 (red)
        // const { x, y } = map.findObject("spawn_one", obj => obj.name === "Spawn Point");
        this.playerRed = new Player(this, 80, 100, 'red');
        this.playerRed.direction = 'right';
        // Create player 2 (blue)
        this.playerBlue = new Player(this, 280, 100, 'blue');
        this.playerBlue.direction = 'left';

        // Create bomb(s)
        this.bomb = this.matter.add.sprite(50, 32, 'bomb', 0);
        this.bomb.setBody({
          type: 'polygon',
          sides: 6,
          radius: 8
        });
        this.bomb.ignoreGravity = true;
        this.bomb.setBounce(1);
        this.bomb.setVelocity(Phaser.Math.Between(-5, 5), 3);
        this.bomb.setFriction(0, 0, 0);
      },

      update() {

      },
    }
  };
  
  var game = new Phaser.Game(config);

Try phaser-on-nodejs.

There is currently something not working with Phaser 3.50.0 and 3.50.1. But you can use Phaser 3.24.1.

See Phaser 3.50 and HEADLESS mode

Thanks @yannick, your suggestion didn’t quite resolve my problem:

  • Uncaught TypeError: Cannot read property 'gl' of null did stop appearing
  • But now, the banner “Phaser v3.24.1 (Headless | HTML 5 Audio)” stopped appearing. It’s as if Phaser wasn’t starting in Headless mode at all.

Does this ring a bell? Again, any suggestion is welcome

Did you do {banner: false} in the config?

Did you do {banner: false} in the config?

No, I hadn’t. Here is my server-side game:

server/authoritative_server/js/game.js
// import Player from './player.js';
import '@geckos.io/phaser-on-nodejs';

const config = {
    type: Phaser.HEADLESS,
    autoFocus: false,
    width: 400,
    height: 240,
    banner: false, // tried true and false
    pixelArt: true,
    zoom: 2,
    physics: {
      default: 'matter',
      matter: {
        gravity: {y: 1.5},
      }
    },
    scene: {
      preload() {
        /* Commented stuff */
      },
      
      create() {
        console.log("Running AUTHORITATIVE");
        /* Commented stuff */
      },

      update() {
        /* Commented stuff */
      },
    }
  };
  
const game = new Phaser.Game(config);
window.gameLoaded();

I suspect the import through import '@geckos.io/phaser-on-nodejs'; is failing, so my server fails silently when I run node server/index.js. But again, I don’t know why:

  • Running node --inspect server/index.js fails silently. Here is the full output:
antoines-mbp:slime_soccer_online alrdebugne$ node --inspect server/index.js 
Debugger listening on ws://127.0.0.1:9229/a3353a65-2efd-4f23-9755-3c18fce04050
For help, see: https://nodejs.org/en/docs/inspector
Debugger attached.
(node:58684) ExperimentalWarning: The fs.promises API is experimental
Waiting for the debugger to disconnect...
^C
  • I tried adding const test = require('@geckos.io/phaser-on-nodejs') to my server/index.js to debug. It returns undefined.

Sorry if my questions are self-evident – I try my best, but I’m new to JS/node.

That’s right. It does not return anything.

Please try the “Basic Setup” from here:

Honestly I’m not sure what I’m doing wrong, but I cannot run the basic set-up. Maybe that’s a good place to start. When i try the basic set-up, I still can’t import phaser-on-nodejs:

import '@geckos.io/phaser-on-nodejs'

// set the fps you need
const FPS = 30
global.phaserOnNodeFPS = FPS // default is 60

// your MainScene
class MainScene extends Phaser.Scene {
  constructor() {
    super('MainScene')
  }
  create() {
    console.log('it works!')
  }
}

// prepare the config for Phaser
const config = {
  type: Phaser.HEADLESS,
  width: 1280,
  height: 720,
  banner: false,
  audio: false,
  scene: [MainScene],
  fps: {
    target: FPS
  },
  physics: {
    default: 'arcade',
    arcade: {
      gravity: { y: 300 }
    }
  }
}

// start the game
new Phaser.Game(config)

Leads to:

Uncaught TypeError: Error resolving module specifier “@geckos.io/phaser-on-nodejs”. Relative module specifiers must start with “./”, “../” or “/”.

And when I add the relative path with /path/to/node_modules, instead I get:

Loading module from “http://127.0.0.1:5500/node_modules/@geckos.io/phaser-on-nodejs/” was blocked because of a disallowed MIME type (“text/html”).

Am I doing something fundamentally wrong?

Why do you use import instead of require? Why don’t you include phaser?

I’m sure the video below helps.

Thanks for the link, this helped clear some confusion on my part :slight_smile: (was mixing client-side files, where I used import (because I run them without node) and server-side files, where I used require (because I run them with node)). However, I’m still stuck in the same spot as before:

Could you perhaps spend 2 minutes glossing over the code in this tutorial and tell me if anything looks fishy/off about the suggested implementation? It runs Phaser 3.15.1 in HEADLESS mode without geckos.io. Given your comments, I’m confused as to whether that’s even meant to work. Of course, no worries if you don’t, you’ve already spent lots of time helping me. Thanks a lot!

I will create a nice template for you this evening.

1 Like

Voila. As promised: https://github.com/geckosio/phaser-on-nodejs-example

1 Like

Thank you so much Yannick! I will dissect the example to understand how to apply it to my case.

Short comment, I’ve had to replace

class ServerScene extends Phaser.Scene {
    players = new Map()
    ...

with

class ServerScene extends Phaser.Scene {
    constructor() {
        super();
        this.players = new Map();
    }
    ...

Again, thanks a lot for this example!

1 Like

You’re welcome :slight_smile:

I made that changes and added snapshot interpolation. Have a look!

Do not forget to check the :ballot_box_with_check: on the post that solved your question. Use the :heart: to like a post.

Thanks a lot for your example, really. I’ve finally understood how to handle client/server interactions. I’m somewhat dumbfounded that it took me so long. I may write up my own ‘how to’ once I feel comfortable enough.

One quick question: which version of node do you use? On my own project, I’m running into errors using node v14.15.3: FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal. The error seems related to node-canvas. I’ve opened an issue on their git. In the meantime, it might help to switch to a version of node you’re used to!

Thanks again!

I think this error is produced on your side. It should work with the latest node version.