Phaser: Spine Pluging stop working with multiple containers

Created on 14 Aug 2019  ·  9Comments  ·  Source: photonstorm/phaser

Version

  • Phaser Version: v3.19.0 (WebGL | Web Audio)

Description

Adding a spine to a container works fine, but if you create another container the webGL crash with error:

WebGL: INVALID_OPERATION: bufferSubData: no buffer

Example Test Code

Using this code:
https://labs.phaser.io/edit.html?src=src\spine\spine%20inside%20container.js

If you add a new container BEFORE the container that contains the spines (just add one line var newCont = this.add.container(400, 300)) the program will crash:

var config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#2d2d66',
    scene: {
        preload: preload,
        create: create,
        update: update,
        pack: {
            files: [
                { type: 'scenePlugin', key: 'SpinePlugin', url: 'plugins/SpinePluginDebug.js', sceneKey: 'spine' }
            ]
        }
    }
};

var controls;

var game = new Phaser.Game(config);

function preload ()
{
    this.load.image('logo', 'assets/sprites/phaser.png');

    this.load.setPath('assets/animations/spine/webgl/');

    this.load.spine('boy', 'spineboy-ess.json', 'spineboy.atlas', true);
    this.load.spine('coin', 'coin-pro.json', 'coin.atlas');
}

function create ()
{
    this.add.image(0, 0, 'logo').setOrigin(0);

    var spineBoy = this.add.spine(0, 0, 'boy', 'walk', true).setScale(0.5);
    var coin = this.add.spine(0, 0, 'coin', 'rotate', true).setScale(0.3);


    var newCont = this.add.container(400, 300);
    var container = this.add.container(400, 300, [ spineBoy, coin ]);


    this.tweens.add({
        targets: container,
        angle: 360,
        duration: 6000,
        repeat: -1
    });

    var cursors = this.input.keyboard.createCursorKeys();

    var controlConfig = {
        camera: this.cameras.main,
        left: cursors.left,
        right: cursors.right,
        up: cursors.up,
        down: cursors.down,
        acceleration: 0.06,
        drag: 0.0005,
        maxSpeed: 1.0
    };

    controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
}

function update (time, delta)
{
    controls.update(delta);
}

If you add a container AFTER the container that contains the spines (just add one line var newCont = this.add.container(400, 300)) the program will also stop working well with no error message:

var config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#2d2d66',
    scene: {
        preload: preload,
        create: create,
        update: update,
        pack: {
            files: [
                { type: 'scenePlugin', key: 'SpinePlugin', url: 'plugins/SpinePluginDebug.js', sceneKey: 'spine' }
            ]
        }
    }
};

var controls;

var game = new Phaser.Game(config);

function preload ()
{
    this.load.image('logo', 'assets/sprites/phaser.png');

    this.load.setPath('assets/animations/spine/webgl/');

    this.load.spine('boy', 'spineboy-ess.json', 'spineboy.atlas', true);
    this.load.spine('coin', 'coin-pro.json', 'coin.atlas');
}

function create ()
{
    this.add.image(0, 0, 'logo').setOrigin(0);

    var spineBoy = this.add.spine(0, 0, 'boy', 'walk', true).setScale(0.5);
    var coin = this.add.spine(0, 0, 'coin', 'rotate', true).setScale(0.3);



    var container = this.add.container(400, 300, [ spineBoy, coin ]);
    var newCont = this.add.container(400, 300);


    this.tweens.add({
        targets: container,
        angle: 360,
        duration: 6000,
        repeat: -1
    });

    var cursors = this.input.keyboard.createCursorKeys();

    var controlConfig = {
        camera: this.cameras.main,
        left: cursors.left,
        right: cursors.right,
        up: cursors.up,
        down: cursors.down,
        acceleration: 0.06,
        drag: 0.0005,
        maxSpeed: 1.0
    };

    controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
}

function update (time, delta)
{
    controls.update(delta);
}

Most helpful comment

Ok, it seems i got the source of problem.

Its related on new members of renderer:

WebGLRenderer.newType
WebGLRenderer.nextTypeMatch

It looks like a little bug in a detection algorithm:

// list is a scene.children.list. 
nextTypeMatch = (i < childCount - 1) ? (list[i + 1].type === this.currentType) : false;
//child is an item of list. And list is a scene.children.list. 
 if (child.type!== this.currentType) 
 {
   this.newType = true;
   this.currentType = child.type;
 }

_So if u add an object to the parentContainer it will remove this object from a display list of the scene, and the order of the render-queue will be messed._

The quick solution is to redefine a render function in ur spine objects like this:

create(){

... 
//Code with a problem:
const container = this.add.container(400, 300);
const spineGameObject = this.add.spine(100, 550, 'vine', 'grow', true);
container.add(spineGameObject);
...

//Quick solution:
      const originalRenderWebGL = spineGameObject.renderWebGL;
      spineGameObject.renderWebGL  = function(renderer, ...args) {
                if (this.parentContainer) {
            renderer.nextTypeMatch = false;
            renderer.newType = true;
        }

        return originalRenderWebGL.call (this, renderer, ...args);
      }
}

Better solution is to inherit ur own Spine class with fixes:

import SpineGameObject from "phaser/plugins/spine/src/gameobject/SpineGameObject";

class FixedSpine extends SpineGameObject
{
        .... //Some redefined methods
    renderWebGL (renderer, ...args) {
        if (this.parentContainer) {
            renderer.nextTypeMatch = false;
            renderer.newType = true;
        }

        return super.renderWebGL(renderer, ...args);
    }
}

...//Dont forget to replace original spine class with yours. And reregister your own spine factory.

//You can use my helper
const GOFactory = Phaser.GameObjects.GameObjectFactory;
const GOFactoryProto = GOFactory.prototype;
const register = GOFactory.register;

/**
 * @memberOf Phaser.GameObjects.GameObjectFactory
 * @method register
 * @param {string} type
 * @param {function} creator
 * @param overwrite
 * @param configurable
 */
GOFactory.register = function (type = "type", creator = noop, overwrite = false, configurable = true) {
    if (overwrite) {
        if (!delete GOFactoryProto[type]) {
            return;
        }
    }

    register(type, creator);

    const factory = GOFactoryProto[type];

    if (delete GOFactoryProto[type]) {
        Object.defineProperty(GOFactoryProto, type, {
            value: factory,
            configurable,
            enumerable: true,
            writable: false,
        });
    }
};



//Then use it like this:
const creator = function (x = 0, y = 0, key = "", animationName = "", loop = false) {
...
        let scene = this.scene;

    let spine = new FixedSpine(
        scene,
        scene["spine"], // "spine" is a mapping key for spine plugin.
        x,
        y,
        key,
        animationName,
        loop,
    );

    if (!spine.parentContainer) {
        this.displayList && this.displayList.add && this.displayList.add(spine);
    }

    this.updateList && this.updateList.add && this.updateList.add(spine);
    return spine;
}
Phaser.GameObjects.GameObjectFactory.register("spine", creator, true, false);


//So after that u can simply use this.add.spine(...) like before with ur own spine class.

Hope its gonna help u. Cya

All 9 comments

I have same problem too!!!

me too

Same here
image

W/o another spines in a container looks:
image

Ok, it seems i got the source of problem.

Its related on new members of renderer:

WebGLRenderer.newType
WebGLRenderer.nextTypeMatch

It looks like a little bug in a detection algorithm:

// list is a scene.children.list. 
nextTypeMatch = (i < childCount - 1) ? (list[i + 1].type === this.currentType) : false;
//child is an item of list. And list is a scene.children.list. 
 if (child.type!== this.currentType) 
 {
   this.newType = true;
   this.currentType = child.type;
 }

_So if u add an object to the parentContainer it will remove this object from a display list of the scene, and the order of the render-queue will be messed._

The quick solution is to redefine a render function in ur spine objects like this:

create(){

... 
//Code with a problem:
const container = this.add.container(400, 300);
const spineGameObject = this.add.spine(100, 550, 'vine', 'grow', true);
container.add(spineGameObject);
...

//Quick solution:
      const originalRenderWebGL = spineGameObject.renderWebGL;
      spineGameObject.renderWebGL  = function(renderer, ...args) {
                if (this.parentContainer) {
            renderer.nextTypeMatch = false;
            renderer.newType = true;
        }

        return originalRenderWebGL.call (this, renderer, ...args);
      }
}

Better solution is to inherit ur own Spine class with fixes:

import SpineGameObject from "phaser/plugins/spine/src/gameobject/SpineGameObject";

class FixedSpine extends SpineGameObject
{
        .... //Some redefined methods
    renderWebGL (renderer, ...args) {
        if (this.parentContainer) {
            renderer.nextTypeMatch = false;
            renderer.newType = true;
        }

        return super.renderWebGL(renderer, ...args);
    }
}

...//Dont forget to replace original spine class with yours. And reregister your own spine factory.

//You can use my helper
const GOFactory = Phaser.GameObjects.GameObjectFactory;
const GOFactoryProto = GOFactory.prototype;
const register = GOFactory.register;

/**
 * @memberOf Phaser.GameObjects.GameObjectFactory
 * @method register
 * @param {string} type
 * @param {function} creator
 * @param overwrite
 * @param configurable
 */
GOFactory.register = function (type = "type", creator = noop, overwrite = false, configurable = true) {
    if (overwrite) {
        if (!delete GOFactoryProto[type]) {
            return;
        }
    }

    register(type, creator);

    const factory = GOFactoryProto[type];

    if (delete GOFactoryProto[type]) {
        Object.defineProperty(GOFactoryProto, type, {
            value: factory,
            configurable,
            enumerable: true,
            writable: false,
        });
    }
};



//Then use it like this:
const creator = function (x = 0, y = 0, key = "", animationName = "", loop = false) {
...
        let scene = this.scene;

    let spine = new FixedSpine(
        scene,
        scene["spine"], // "spine" is a mapping key for spine plugin.
        x,
        y,
        key,
        animationName,
        loop,
    );

    if (!spine.parentContainer) {
        this.displayList && this.displayList.add && this.displayList.add(spine);
    }

    this.updateList && this.updateList.add && this.updateList.add(spine);
    return spine;
}
Phaser.GameObjects.GameObjectFactory.register("spine", creator, true, false);


//So after that u can simply use this.add.spine(...) like before with ur own spine class.

Hope its gonna help u. Cya

Ok, it seems i got the source of problem.

Its related on new members of renderer:

WebGLRenderer.newType
WebGLRenderer.nextTypeMatch

It looks like a little bug in a detection algorithm:

// list is a scene.children.list. 
nextTypeMatch = (i < childCount - 1) ? (list[i + 1].type === this.currentType) : false;
//child is an item of list. And list is a scene.children.list. 
 if (child.type!== this.currentType) 
 {
   this.newType = true;
   this.currentType = child.type;
 }

_So if u add an object to the parentContainer it will remove this object from a display list of the scene, and the order of the render-queue will be messed._

The quick solution is to redefine a render function in ur spine objects like this:

create(){

... 
//Code with a problem:
const container = this.add.container(400, 300);
const spineGameObject = this.add.spine(100, 550, 'vine', 'grow', true);
container.add(spineGameObject);
...

//Quick solution:
      const originalRenderWebGL = spineGameObject.renderWebGL;
      spineGameObject.renderWebGL  = function(renderer, ...args) {
                if (this.parentContainer) {
          renderer.nextTypeMatch = false;
          renderer.newType = true;
      }

      return originalRenderWebGL.call (this, renderer, ...args);
      }
}

Better solution is to inherit ur own Spine class with fixes:

import SpineGameObject from "phaser/plugins/spine/src/gameobject/SpineGameObject";

class FixedSpine extends SpineGameObject
{
        .... //Some redefined methods
  renderWebGL (renderer, ...args) {
      if (this.parentContainer) {
          renderer.nextTypeMatch = false;
          renderer.newType = true;
      }

      return super.renderWebGL(renderer, ...args);
  }
}

...//Dont forget to replace original spine class with yours. And reregister your own spine factory.

//You can use my helper
const GOFactory = Phaser.GameObjects.GameObjectFactory;
const GOFactoryProto = GOFactory.prototype;
const register = GOFactory.register;

/**
 * @memberOf Phaser.GameObjects.GameObjectFactory
 * @method register
 * @param {string} type
 * @param {function} creator
 * @param overwrite
 * @param configurable
 */
GOFactory.register = function (type = "type", creator = noop, overwrite = false, configurable = true) {
  if (overwrite) {
      if (!delete GOFactoryProto[type]) {
          return;
      }
  }

  register(type, creator);

  const factory = GOFactoryProto[type];

  if (delete GOFactoryProto[type]) {
      Object.defineProperty(GOFactoryProto, type, {
          value: factory,
          configurable,
          enumerable: true,
          writable: false,
      });
  }
};



//Then use it like this:
const creator = function (x = 0, y = 0, key = "", animationName = "", loop = false) {
...
        let scene = this.scene;

  let spine = new FixedSpine(
      scene,
      scene["spine"], // "spine" is a mapping key for spine plugin.
      x,
      y,
      key,
      animationName,
      loop,
  );

  if (!spine.parentContainer) {
      this.displayList && this.displayList.add && this.displayList.add(spine);
  }

  this.updateList && this.updateList.add && this.updateList.add(spine);
  return spine;
}
Phaser.GameObjects.GameObjectFactory.register("spine", creator, true, false);


//So after that u can simply use this.add.spine(...) like before with ur own spine class.

Hope its gonna help u. Cya

Hey, thanks for the solution :). Is any way we can solve this directly in Phaser or in the Spine Pluging?

Hey, thanks for the solution :). Is any way we can solve this directly in Phaser or in the Spine Pluging?

Sure.

Remove conditions at the lines:

  • phaser/plugins/spine/src/gameobject/SpineGameObjectWebGLRenderer.js:40
  • phaser/plugins/spine/src/gameobject/SpineGameObjectWebGLRenderer.js:50
  • phaser/plugins/spine/src/gameobject/SpineGameObjectWebGLRenderer.js:136

_(remove only 'if(...)' and keep code in a brackets)_

Thanks for posting a work-around @justsl - be warned, using this approach will break Spine batching, causing a render flush for every container in your game. This may, or may not, matter, depending on how many you have.

Is there any plan to fix this?

Thank you for submitting this issue. We have fixed this and the fix has been pushed to the master branch. It will be part of the next release. If you get time to build and test it for yourself we would appreciate that.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

samme picture samme  ·  4Comments

JarLowrey picture JarLowrey  ·  4Comments

MarkSky picture MarkSky  ·  3Comments

maikthomas picture maikthomas  ·  4Comments

sercand picture sercand  ·  3Comments