Doug

Calling an Angular function from within Phaser 3 scene

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

Share this post


Link to post
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 :-)

Share this post


Link to post
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!

Share this post


Link to post
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.

Share this post


Link to post
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!

Share this post


Link to post
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);
  }

}

 

Share this post


Link to post
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!

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
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!

Share this post


Link to post
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!

Share this post


Link to post
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.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.