Skip to content
Advertisement

Phaser 3 How to Access and Affect Player in Collision between 2 Objects Not Involving Player

I am trying to create a grappling hook in Phaser 3. I can successfully shoot the grappling hook at an upward angle away from the player. I need to get the player to move towards the grappling hook when the grappling hook reaches its destination. The problem is that I can’t access the player object in my Spawner.ts file to move the player object. If I could access the player object I could set the velocity at the moment of collision between the grappling hook and the level.

This is the relevant code in the Spawner.ts file:

import { Actor } from "./Actor";
import { Level } from "./Level";
import { Layer } from "./shared";
import { Projectile } from "./Projectile/Projectile";
import { GameScene } from "../scenes/GameScene";
import { Player } from "./Player";
import { GrapplingHook } from "./GrapplingHook";

interface ActorConstructor {
    new (scene: Phaser.Scene, x: number, y: number): Actor;
}

interface GrapplingHookContructor {
    new (scene: GameScene, x: number, y: number): GrapplingHook;
}

export class Spawner {
    private projectiles: Phaser.GameObjects.Group;
    private actors: Phaser.GameObjects.Group;
    private grapplinghooks: Phaser.GameObjects.Group;

    constructor(private scene: GameScene, private level: Level) {
        this.actors = this.scene.add.group();
        this.setUpGrapplingHooks();
    }

    spawnDynamic<T extends ActorConstructor>(
        layer: Layer,
        type: T
    ): Phaser.GameObjects.Group {
        const objects = this.level.getObjects(layer);
        const instances = objects.map((e) => new type(this.scene, e.x, e.y));
        const group = this.scene.add.group(instances);
        this.level.addGroundCollision(group);
        return group;
    }

    spawnPlayer(layer: Layer): Player {
        const player = this.spawnDynamic(
            layer,
            Player
        ).getChildren()[0] as Player;

        this.actors.add(player);

        return player;
    }

    spawnGrapplingHook<T extends GrapplingHookContructor>(
        type: T,
        x: number,
        y: number,
        xVelocity = 0,
        yVelocity = 0
    ): void {
        const grapplinghook = new type(this.scene, x, y);
        this.grapplinghooks.add(grapplinghook);
        grapplinghook.body.setVelocity(xVelocity, yVelocity);
        grapplinghook.body.setCollideWorldBounds(true, undefined, undefined, true);
    }

    destroyGrapplingHooks() {
        this.grapplinghooks.getChildren().map(child => child.destroy());
    }

    private getObjectData(
        object: Phaser.Types.Tilemaps.TiledObject
    ): Record<string, unknown> {
        const props = object.properties as unknown;
        const data: Record<string, unknown> = {};

        if (props instanceof Array) {
            props.forEach((p: { name: string; value: unknown }) => {
                data[p.name] = p.value;
            });
        }

        return data;
    }

    private setUpGrapplingHooks() {
        this.grapplinghooks = this.scene.add.group();
        this.grapplinghooks.runChildUpdate = true;
        this.level.addGroundCollision(this.grapplinghooks, (grapplinghook) =>
            (grapplinghook as GrapplingHook).onLevelCollide()
        );

        this.scene.physics.add.collider(
            this.grapplinghooks,
            this.level.getLayer(Layer.Bricks),
            (grapplinghook) => (grapplinghook as Projectile).onLevelCollide()
        );

        this.scene.physics.add.collider(
            this.grapplinghooks,
            this.actors,
            (grapplinghook, entity) => {
                (grapplinghook as Projectile).onCollide(entity as Actor);
            }
        );
    }
}

This is the relevant code in the Player.ts file:

import { TILE_WIDTH } from "./shared";
import type { GameScene } from "../scenes/GameScene";
import { Actor } from "./Actor";
import { Assets } from "./shared";
import { GrapplingHook } from "./GrapplingHook";

interface Controls {
    Left: Phaser.Input.Keyboard.Key;
    Right: Phaser.Input.Keyboard.Key;
    Jump: Phaser.Input.Keyboard.Key;
    ThrowGrapplingHook: Phaser.Input.Keyboard.Key;
}

export class Player extends Actor {
    protected readonly totalHitPoints = 3;
    protected readonly immunityAfterDamageTime: number = 1000;

    private controls: Controls;

    constructor(scene: GameScene, x: number, y: number) {
        super(scene, x, y, Assets[Assets.Player], 1);

        // controls
        this.controls = {
            Left: scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A),
            Right: scene.input.keyboard.addKey(
                Phaser.Input.Keyboard.KeyCodes.D
            ),
            Jump: scene.input.keyboard.addKey(
                Phaser.Input.Keyboard.KeyCodes.SPACE
            ),
            ThrowGrapplingHook: scene.input.keyboard.addKey(
                Phaser.Input.Keyboard.KeyCodes.H
            )
        };
    }

    /**
     * Throws a projectile at a given angle and force
     * @param time The current game time
     * @param angle The angle to throw the projectile; Should be a value between -90 (straight down) and 90 (straight up)
     * @param force What velocity to throw the projectile at
     */

    protected throwGrapplingHook(time: number, angle: number, force: number) {
        if (angle > 90 || angle < -90) {
            throw `throwProjectile(angle) must be between -90 and 90; current value: ${angle}`;
        }

        this.lastProjectileThrowTime = time;

        let x = this.body.x;
        if (this.flipX) {
            x = x + TILE_WIDTH;
        }

        // calculate the x and y force based on angle and total force
        // angle: 0 -> x velocity at 100%, y at 0%
        // angle: 90 -> x velocity at 0%, y at 100%

        const percentYForce = angle / 90;
        const yVelocity = force * percentYForce * -1;
        let xVelocity = force - Math.abs(yVelocity);

        if (this.body.velocity.x < 0) {
            xVelocity *= -1;
        }

        this.scene.spawner.spawnGrapplingHook(
            GrapplingHook,
            x,
            this.body.y,
            xVelocity,
            yVelocity
        );
    }

    protected onUpdate(): void {
        if (!this.active) {
            return;
        }
    }
}

And this is the relevant code from the GrapplingHook.ts file:

import type { Actor } from "./Actor";
import { Assets } from "./shared";

export class GrapplingHook extends Phaser.Physics.Arcade.Sprite {
    declare body: Phaser.Physics.Arcade.Body;

    constructor(scene: Phaser.Scene, x: number, y: number) {
        super(scene, x, y, Assets[Assets.Projectiles], 1);

        scene.add.existing(this);
        scene.physics.add.existing(this);
        this.body.setAllowGravity(false);

        this.body
            .setSize(this.body.width, this.body.height - 20, true)
            .updateCenter()
    }

    onCollide(target: Actor): void {
        this.destroy();
    }

    onLevelCollide(): void {
        this.setVelocity(0,0);
    }

    update(): void {
        this.flipX = this.body.velocity.x >= 0
    }
}

As it is now, the code successfully throws the grappling hook but I can’t actually move the player after it collides with the level. I haven’t even looked into how to move the player towards the actually collided grappling hook, as a proof of concept I just want to nudge the player forward when the collision happens. My instincts are to change the onLevelCollide function in GrapplingHook.ts to:

onLevelCollide(player: Player): void {
        player.setVelocityX(100);
        this.setVelocity(0,0);
    }

And then add a player object into onLevelCollide() when it’s called in Spawner.ts but I can’t seem to access the player in Spawner.ts. How would I pass the player object into onLevelCollide or possibly work around this problem another way?

Again I am trying to move the player on the grappling hook’s collision with the level. If you need me to post more code or clarify let me know.

Advertisement

Answer

Just because I was interested, on how I would approach this problem myself, I adapted (I stripped the most stuff out, that is not really needed) this phaser example (First Game) to acomplish the grappling hook mechanic.

I’m just sharing it, because it is a small working example covering the key task:

  • shooting the hook
  • managing the velocity, before / during / after the hook-action
  • pulling the player towards the hook

Yes, it is only javascript without classes(and …), but it still illustrates the main point nicely (I think).

Controls:

  • left mouse button: shoot “hook”
  • left / Right : move/walk player (animation was removed)
  • SHIFT retract grappling hook

var config = {
        type: Phaser.AUTO,
        width: 400,
        height: 300,
        physics: {
            default: 'arcade',
            arcade: {
                gravity: { y: 150 }
            }
        },
        scene: {
            preload: preload,
            create: create,
            update: update
        }
    };

    var player;
    var platforms;
    var cursors;

    // basic Hook Object
    var hook = {
        isPulling: false,
        isAttached: false,
        speed: 500,
        set display(value){
          this.gameObject.visible = value;
          this.ropeGameObject.visible = value;
        }
    }

    var game = new Phaser.Game(config);

    function preload ()
    {
        this.load.image('ground', 'https://labs.phaser.io/src/games/firstgame/assets/platform.png');
        this.load.image('star', 'https://labs.phaser.io/src/games/firstgame/assets/star.png');
        this.load.spritesheet('dude', 'https://labs.phaser.io/src/games/firstgame/assets/dude.png', { frameWidth: 32, frameHeight: 48 });
    }

    function create ()
    {
    
        let txt = this.add.text(0, 20, '> click mouse to shoot hooknt>> press "shift" to pull');
        platforms = this.physics.add.staticGroup();
        platforms.create(200, 310, 'ground');
        platforms.create(200, -10, 'ground');
        platforms.create(400, 100, 'ground').setScale(.5).refreshBody();
        platforms.create(50, 175, 'ground').setScale(.5).refreshBody();
        
        // Setup Hook Rope
        hook.ropeGameObject = this.add.line(0,0, 0,0, 10,10, 0xff0000).setOrigin(0);
        
        // Setup Hook
        hook.gameObject = this.physics.add.sprite(100, 450, 'star').setScale(.3);
        hook.gameObject.body.allowGravity = false;
        
        // Hide Hook
        hook.display = false;
        
        // Setup Hook Collision
        this.physics.add.collider(hook.gameObject, platforms, grab);
        
        player = this.physics.add.sprite(100, 450, 'dude',4).setScale(.75);
        player.setCollideWorldBounds(true);
        player.setBounce(0.2);
        this.physics.add.collider(player, platforms);
        
        // Setup User Controls
        cursors = this.input.keyboard.createCursorKeys();
        this.input.on('pointerdown', shootHook, this);
    }

    function grab(hk, platform){
        hook.gameObject.setVelocity(0);
        hook.isAttached = true;
    }

    function shootHook(pointer){
        if(!hook.isPulling){
            let velocity = new Phaser.Math.Vector2(pointer.x - player.x, pointer.y - player.y)
                .normalize()
                .scale(hook.speed);
            hook.gameObject.x = player.x;
            hook.gameObject.y = player.y;
            hook.display = true;
            hook.isAttached = false;  
            hook.gameObject.setVelocity(velocity.x, velocity.y);
        }
    }

    function updateHookRope(){
        hook.ropeGameObject.setTo(player.x, player.y,hook.gameObject.x,hook.gameObject.y);
    }

    function update ()
    {
        updateHookRope();
        if(hook.isAttached && cursors.shift.isDown){
            hook.isPulling = true;
            let pullVelocity = new Phaser.Math.Vector2( hook.gameObject.x - player.x, hook.gameObject.y - player.y)
                .normalize()
                .scale(hook.speed / 1.5);
            player.setVelocity(pullVelocity.x, pullVelocity.y);
            
        } else if(hook.isPulling) { // Hook was released so remove it
          if(hook.isAttached){
             hook.isAttached = false;
             hook.display = false;
           }
           hook.isPulling = !player.body.touching.down;
        }

        if(hook.isPulling){
            return;
        }

        if (cursors.left.isDown) {
            player.setVelocityX(-160);
        }
        else if (cursors.right.isDown) {
            player.setVelocityX(160);
        }
        else {
            player.setVelocityX(0);
        }

    }
<script src="//cdn.jsdelivr.net/npm/phaser@3.55.2/dist/phaser.min.js"></script>
User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement