Jump to content

Calling an Angular function from within Phaser 3 scene


Doug
 Share

Recommended Posts

Hi all

I'm using Angular4 with Phaser 3 beta 20.  I need to have separate classes for the Game Scenes so that I can use different game scenes any time I wish.

I need to be able to call a function in the parent component that affects the parent component variables.

This is probably a totally newbie question so please forgive me, but it's stumped me for the moment on how the scope works.  Please see the following code:

import { Component } from '@angular/core';

import Phaser from 'phaser';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  
  phaserGame: Phaser.Game;
  carrot: any;
  preloadStatus: string = "Preloading";
  


  config: any;
  gameScene: Phaser.Class;


  getScene() : any{
      return {
          Extends: Phaser.Scene,

          initialize:            

          function GameScene(config) {
              Phaser.Scene.call(this, config);
          },

          preload: function () {
              this.load.image('carrot', 'assets/carrot.png');                
              console.log("Phaser game loading done");  
              this.FromInGame();    
          },

          create: function () {
              this.carrot = this.add.image(400, 300, 'carrot'); 
          },

          update: function () {                
              this.carrot.x += .2;
          }
      }
  }


  constructor() { 

    let theScene = this.getScene();
    theScene.FromInGame = this.FromInGame;
    this.gameScene = new Phaser.Class(theScene);
    

    this.config = {
      type: Phaser.CANVAS,
      parent: 'phaser-example',
      scene: this.gameScene
    };
    this.phaserGame = new Phaser.Game(this.config);
  }



  FromInGame() : void{
    console.log("called from in game");
    this.preloadStatus = "Preload DONE!";
  }

}

I need the FromInGame() function to be fired from the Scene's preload() function, which it is.  However, in the FromInGame() function "this" clearly references the Scene because "theScene.FromInGame = this.FromInGame;" is setting it as a function on the Scene.

Therefore when executing "this.preloadStatus = "Preload DONE!";" it adds a property preloadStatus with a value "Preload DONE!" to the Scene and rather than updating the this.preloadStatus in the Angular component.

Any ideas how I would achieve this please?

Thank you very much in advance.

D

Link to comment
Share on other sites

What I would try: You need to add "fromInGame" to the gameScene after you created the class. Then you'll have a new problem when Phaser fires the scene and you haven't added "fromInGame" yet, so you need to do something in your scene that checks if "fromInGame" is avaliable and fire it when ready.

Let me know if it works :-)

Link to comment
Share on other sites

Thanks @nkholski much appreciated.  

I have now had a much closer look at this.  I've changed my approach a little and am now using "Scene From Es6 Class" approach in demo http://labs.phaser.io/view.html?src=src\scenes\scene from es6 class.js

I can now pass the "FromInGame" function into the class constructor, assign it to a variable in the class and then call it to fire the parent function.

However, it still maintains scope WITHIN the phaser scene and has no direct access to the properties in the Angular component.  So although I can call it, I can't do anything to the main page with it.

I CAN, however, pass a variable from the Angular component to the Scene constructor in the same way.  Changing that variable directly within the Scene class has the same scope problem as the function being passed in.  But if I pass that variable as an object with it's own properties within it, updating those properties DOES update the variable properties in the Angular component :)

However, I now have a new problem!

Using this approach now seems to make the scene function calls such as...

this.load.image('carrot', 'assets/carrot.png');

...fail to compile, with error...

Property 'load' does not exist on type 'MyScene'.

This seems odd as MyScene extends Phaser.Scene.  I also tried...

super.load.image('carrot', 'assets/carrot.png');

This compiles, but gives me the following error in the console (i.e. the same as above):

Property 'load' does not exist on type 'MyScene'.

So the question now is, how do I get these calls to work?

Here is my code:

HTML:

<div id="phaser-example"></div>

{{preloadStatus.message}}

TS:

import { Component } from '@angular/core';

import Phaser from 'phaser';

class MyScene extends Phaser.Scene {
    FromInGame: any;
    preloadStatus: any;
    constructor (config, FromInGame, preloadStatus)
    {
        super(config);
        this.preloadStatus = preloadStatus;
        this.FromInGame = FromInGame;
    }

    preload ()
    {
        console.log("preload");  
        this.load.image('carrot', 'assets/carrot.png'); //<<<<<<<<<<<<<THIS BREAKS IT!!
        super.load.image('carrot', 'assets/carrot.png'); //<<<<<<<<<<<<<THIS BREAKS IT TOO!!
    }

    create ()
    {
        console.log("create");
        this.preloadStatus.message = "Fraggles rock!";
        this.FromInGame(); 
    }

    update(){}

}
    

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  
  phaserGame: Phaser.Game;
  preloadStatus: any = { message: 'preloading' };

  config: any;    

  constructor() { 
    this.config = {
      type: Phaser.CANVAS,
      parent: 'phaser-example'
    };

    this.config.scene = new MyScene(this.config, this.FromInGame, this.preloadStatus);

    this.phaserGame = new Phaser.Game(this.config);    
  }


  FromInGame() : void{
    console.log("called from in game");
  }

}

Thanks very much!

Link to comment
Share on other sites

OK so I figured this out.  It seems that TypeScript just didn't know about the "load" methods on Phaser.Scene even though they are there.  If you declare them on the Class as "any" then it knows about them and works fine.

class MyScene extends (Phaser.Scene as { new(config): any; }) {
  FromInGame: any;
  preloadStatus: any;
  load: any; // << ADDED THIS
  add: any; // << ADDED THIS
  constructor (config, FromInGame, preloadStatus)
  {
      super(config);
      this.preloadStatus = preloadStatus;
      this.FromInGame = FromInGame;
  }

  preload ()
  {
      console.log("preload");  
      this.load.image('carrot', 'assets/carrot.png'); // <<<NOW WORKS FINE
      
  }

  create ()
  {
      console.log("create");
      this.add.image(400, 300, 'carrot');
      this.preloadStatus.message = "Fraggles rock!";
      this.FromInGame();
  }

  update(){}

}

I'm still having some scope issues calling the FromInGame() function in the parent component because although it fires, it can't access other methods etc.  I'll probably ask more on this one as I delve further into it.

Link to comment
Share on other sites

I still haven't found a way to deal properly with scope and calling functions in the parent component from a Phaser Scene class.  However, I did find a way around it, and thought I'd post it in case it's helpful to others.

Please see https://blog.lacolaco.net/post/event-broadcasting-in-angular-2/

It seems that you can set up an event broadcaster as per the link above.  You broadcast the event from the Phaser Scene Class and listen for it in the parent Angular component.  Works a charm.

Hope that helps!

Link to comment
Share on other sites

An idea if you don't mind adding a global var you could define something like "window.angularLink = this" in the angular component, then you can access the components variables and run methods from phaser (like window.angularLink.sayHello("hello") within the phaser scene), and you could pass methods from phaser back again to allow the component do stuff with Phaser (window.angularLink.aPhaserMethod = this.someMethod.bind(this) in the Phaser scene, and then call it by window.angularLink.aPhaserMethod() from the angular component). It might not be a very clean solution but it'll work.

You could try to pass "this" from the component via the config too. That might work somehow and avoid setting a global variable.

import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import * as Phaser from 'phaser';
import { ShowSprite } from './scenes/myScene';

@Component({
  selector: 'app-phaser',
  templateUrl: './phaser.component.html',
  styleUrls: ['./phaser.component.css']
})
export class PhaserComponent {
  config: any = {
    type: Phaser.AUTO,
    custom: 'hejsan',
    parent: 'content',
    width: 400,
    height: 240,
    physics: {
      default: 'arcade',
      arcade: {
        gravity: { y: 800 },
        debug: false
      }
    },
    scene: [
      myScene
    ]
  };


  constructor() {


  }

  ngOnInit() {
    let game = new Phaser.Game(this.config);
    window["angularLink"] = this;
  }

  sayHello(str: string) {
    alert(str);
  }

}

 

Link to comment
Share on other sites

Thank you again for a very comprehensive reply.  Much appreciated. :)

I tried passing this in with the config before and strangely the scope was still lost that way too.

The global var route is an interesting approach for sure.  I'll continue to have a play :)

Thanks!

Link to comment
Share on other sites

  • 4 weeks later...

Hi Doug,

I actually stumbled across your comment because I am starting to use Phaser 3 with Angular and am getting the bare bones setup right now. This comment thread is the first thing that Google returns for "phaser 3 angular", funny enough. I am curious if you were able to resolve the issues you were seeing before? I have quite a bit of experience with Angular and am already planning out how I want to setup my application, unless there are some serious things that need to be worked around with Angular. I'll probably be handling what you were talking about with shared services that can be subscribed to, thus removing the need to try and access properties off of another component and allowing me to "push" whatever properties I want onto the Phaser objects from anywhere in the application.

Link to comment
Share on other sites

Hi @LordLants - I did get it working, but I took a rather unconventional route to do it in the end.

I'm using game classes so that I can keep the games modular.  I pass the context into the class constructor and then set a local variable to store that context.  The following is cut down, but gives the idea:

export class GAME_SpaceShooter extends (Phaser.Scene as { new(config): any; }) { 

    constructor (
        config, 
        context
    )
    {        
        super(config);
        this.context = context;
    }

    sendGameEvent(gameEvent: string) : void
    {
        this.context.gameEvent.fire(gameEvent);
    }

}

I then have an interface as follows based on this article:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import {Broadcaster} from './broadcastEvent';

@Injectable()
export class GameEvent {
  constructor(private broadcaster: Broadcaster) {}

  fire(data: string): void {
    this.broadcaster.broadcast(MessageEvent, data);
  }

  on(): Observable<string> {
    return this.broadcaster.on<string>(MessageEvent);
  }
}

I import the interface into my main page class and subscribe to the gameEvent:

...

import { GameEvent } from '../../interfaces/gameEvent';

@Component({
  selector: 'app-game',
  templateUrl: './game.component.html',
  styleUrls: ['./game.component.css']
})
export class GameComponent implements OnInit {

  constructor(
    public gameEvent: GameEvent
  ) { }

  getGameStartDataProcessed(response: any, allOk: Boolean): void {//this is called after the XHR returns some app specific data from the server
           this.gameEventSubscribe = this.gameEvent.on().subscribe(eventName => {
            switch (eventName) {
              case 'eventId1':          
                //do stuff
                break;
              case 'eventId2':          
                //do other stuff
                break;
              default:
                break;
            }            
          });
  }

}

This works fine for me.

I think I got everything in the description above, but let me know if you have any problems.

Good luck!

Link to comment
Share on other sites

@Doug Thank you much for all the insightful code snippets!
On an off topic, I actually saw your issue thread on Github in regards to getting Phaser 3 working with Angular and am confused how you got the phaser.min.js to work without using "ng eject". If at any point I added phaser.min.js as a script in the index.html or in the scripts in the .angular-cli.json file it would blow up when trying to bundle the .frag and .vert portions of the code (with the hashtag). I eventually did get it to work with "ng eject", adding the necessary code to the ejected webpack.config.js file (both the loader and config changes), but being able to get everything working without needing to eject would be great.

Thanks!

Link to comment
Share on other sites

Yeah, I was very keen NOT to eject the project because it makes things a lot more complicated.  I did manage to get it working via that route by making some changes to the webpack config file that gets generated when you eject the project.  However, it wasn't much fun working with the project once ejected.

I ended up NOT ejecting the project but just including the phaser JavaScript file in the "scripts" section of .angular-cli.json.  Then in the component where I use phaser I added:

declare var Phaser: any;

I think that was everything I did.  It works a treat and means I can easily update the phaser JavaScript file by just replacing it rather than using npm as Rich iterates the updates and fixes bugs etc.  :)

I hope that helps and let me know if I missed anything.

Link to comment
Share on other sites

  • 10 months later...

I am just taking Phaser for a spin for the first time ever, immediately ran into the problem you describe, and then on to this thread. I ended up finding an alternate solution and just thought I would share. There is a way to have your cake and it it too... First, let's start with my constructor and instantiation of a game config object:

    public constructor() {
        const that = this;
        this.gameConfig = {
            type: Phaser.AUTO,
            width: 800,
            height: 600,
            scene: {
                preload: function() {
                    that.preloadScene(this);
                },
                create: function() {
                    that.createScene(this);
                },
                update: function() {
                    that.updateScene(this);
                },
            },
            physics: {
                default: 'arcade',
                arcade: {
                    gravity: { y: 300 },
                    debug: false
                }
            },
        };
    }

I take a simple step in the callbacks to allow myself access to *both* contexts - the scene and the surrounding component. I do the old classic "that = this;" Then, within the callback definitions I do: "that.preloadScene(this);" The "this" that I am calling the component's functions with is the callback context, so not the component, but the scene. Hence, I can implement those functions like:

    createScene(scene) {
        scene.add.image(400, 300, 'sky');
        this.createPlatforms(scene);
        this.createPlayer(scene);
    }

Now you can see I have access to both the scene (passed as an argument) and also the component, which is accessible via "this". Therefore, I can call other functions defined on that component and I can also pass the scene object around to them to operate on as well.

Hope this is useful to someone!

Link to comment
Share on other sites

  • 8 months later...
  • 5 months later...

About how to calling angular functions within a phaser scene, you should be able to do that if you save the context before to initialize the game. Check a little example with IonPhaser WebComponent:

 

import { Component } from '@angular/core'
import * as Phaser from 'phaser'
import { ApiService } from '../services';

@Component({
  selector: 'my-component',
  templateUrl: './my-component.html'
})
export class MyComponent {
  game: Phaser.Game
  initialize = false

  constructor(
    private readonly api: ApiService
  ){}

  initializeGame() {
    const context = this
    context.initialize = true
    context.game  = {
      width: "100%",
      height: "100%",
      type: Phaser.AUTO,
      scene: {
        create() {
          this.helloWorld = this.add.text(0, 0, 'Hello World')
        }
        update () {
          this.helloWorld.angle += 1;
        }
        saveGame() {
          const { angle } = this.helloWorld
          context.api.saveAngle(angle)
        }
      }
    }
  }
}

Other options can be using event emitters, injecting the component context, etc :)

For any other question, please check the repository of the project: https://github.com/proyecto26/ion-phaser/issues/15

Kind regards,
Juan

Edited by jdnichollsc
Link to comment
Share on other sites

 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...