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>