Sprite animation stuck on first frame? - javascript

Working with Phaser 3, I've preloaded a spritesheet and created a few animations...
import {Scene} from 'phaser';
class BootScene extends Scene {
constructor() {
super("scene-boot");
}
preload() {
this.load.spritesheet('px-hero', 'assets/sprites/px-hero.png', {
frameWidth: 16,
frameHeight: 16
});
// ...
}
create() {
// ...
this.anims.create({
key: 'px-hero-idle',
frames: this.anims.generateFrameNumbers('px-hero', {
start: 0,
end: 2
}),
frameRate: 10,
repeat: -1
});
this.anims.create({
key: 'px-hero-run',
frames: this.anims.generateFrameNumbers('px-hero', {
start: 3,
end: 6
}),
frameRate: 10,
repeat: -1
});
// ...
}
}
export default BootScene;
Then inside my Sprite class (which is being instantiated in another scene that the BootScene links to), I'm trying to play the animations...
import {GameObjects} from 'phaser';
const {Sprite} = GameObjects;
class PxHero extends Sprite {
constructor(config) {
super(config.scene, config.x, config.y, "px-hero");
// Add self to scene's physics
config.scene.physics.world.enable(this);
config.scene.add.existing(this);
this.scene = config.scene;
this.keys = this.scene.input.keyboard.addKeys('W,S,A,D');
this.speed = 100;
this.jumpHeight = 300;
}
preUpdate(time, delta) {
const {W, S, A, D} = this.keys;
const {speed, jumpHeight, body} = this;
const touchingGround = body.blocked.down;
if (A.isDown) {
this.body.setVelocityX(-speed);
this.setFlipX(true);
}
else if (D.isDown) {
this.body.setVelocityX(speed);
this.setFlipX(false);
}
else {
this.body.setVelocityX(0);
}
if (W.isDown && touchingGround) {
this.body.setVelocityY(-jumpHeight);
}
// Animations
if (touchingGround) {
if (body.velocity.x !== 0) {
this.anims.play('px-hero-run', true); // here
}
else {
this.anims.play('px-hero-idle', true); // and here
}
}
}
}
export default PxHero;
But for some reason they just play the first frame of the animation then get stuck there.
Has anyone encountered this before? I haven't been able to find any solutions thus far.

Per frame animations are working based on preUpdate function, it's not the best idea to do something yours in that function without calling super.preUpdate. That's first, second in place of preUpdate try to use update function, and don't forget to call super.update in it.
And third, try to do what you want without using preUpdate and update functions. If you even need to use update function, you can do that with setting listener on scene to update (Phaser.Scenes.Events.UPDATE) event. Which will make your code more clear and understandable, and you'll never harm Phaser's main functionality by mistake.
What happens in your code is the following, on every frame rendering, your game checks and decides to run an animation from start, that's why you're seeing first frames only, just because when it tries to show you next frame you're telling him to start playing animation again )
export default class PxHero extends Phaser.GameObjects.Sprite{
constructor(config) {
super(config.scene, config.x, config.y, "px-hero");
// Add self to scene's physics
// ## you've `this` here too
this.scene.add.existing(this);
this.scene.physics.world.enable(this);
this.prepare();
this.setListeners();
}
prepare(){
this.keys = this.scene.input.keyboard.addKeys('W,S,A,D');
this.speed = 100;
this.jumpHeight = 300;
}
setListeners(){
this.scene.events.on(Phaser.Scenes.Events.UPDATE, this.handleMovement, this)
}
handleMovement() {
const {W, S, A, D} = this.keys;
const {speed, jumpHeight, body} = this;
const touchingGround = body.blocked.down;
switch(true){
case A.isDown:
this.body.setVelocityX(-speed);
this.setFlipX(true);
break;
case D.isDown:
this.body.setVelocityX(speed);
this.setFlipX(false);
break;
case S.isDown:
this.body.setVelocityX(0);
break;
case W.isDown && touchingGround:
this.body.setVelocityY(-jumpHeight);
break;
}
// Animations
if (touchingGround) {
if (body.velocity.x !== 0) {
(!this.anims.isPlaying || this.anims.key !== 'px-her-run') &&
this.anims.play('px-hero-run', true); // here
}
else {
(!this.anims.isPlaying || this.anims.key !== 'px-her-idle') &&
this.anims.play('px-hero-idle', true); // and here
}
}
}
}

Related

Chebyshev distance x coordinate bugs when moving

So I was replicating the Phaser Chebyshev Distance example, And After a solution here
I encounter the error Cannot read properties of null (reading 'x') for origin.x,
And the strange part is, this happens after I travel a few tiles from x=0
code
import Phaser from "../lib/phaser.js";
export default class Game extends Phaser.Scene {
constructor() {
super("game1");
}
preload() {
this.load.image(
"Tileset",
"https://cdn.glitch.global/cc90578e-c3d0-47c5-bb0d-f5a81263b5b6/pixil-frame-0%20(17).png?v=1675985219390"
);
this.load.tilemapTiledJSON(
"map",
"https://cdn.glitch.global/cc90578e-c3d0-47c5-bb0d-f5a81263b5b6/Map.tmj?v=1675985261369"
);
this.load.spritesheet(
"player",
"https://cdn.glitch.global/cc90578e-c3d0-47c5-bb0d-f5a81263b5b6/pixil-frame-0%20(13).png?v=1675904194091",
{ frameWidth: 32, frameHeight: 32 }
);
}
create() {
this.gameState = {
map: "",
cursors: "",
};
this.gameState.map = this.make.tilemap({ key: "map" });
this.tiles = this.gameState.map.addTilesetImage("tileSet", "Tileset");
this.worldLayer = this.gameState.map.createLayer(
"Block Layer",
this.tiles,
0,
0
);
this.player = this.physics.add.sprite(0, 1600, "player", 1);
this.cameras.main.setBounds(
0,
0,
this.gameState.map.widthInPixels,
this.gameState.map.heightInPixels
);
this.cameras.main.startFollow(this.player);
this.gameState.cursors = this.input.keyboard.createCursorKeys();
this.physics.add.collider(this.player, this.worldLayer);
}
update() {
if (this.gameState.cursors.left.isDown) {
this.player.setVelocityX(-50);
} else if (this.gameState.cursors.right.isDown) {
this.player.setVelocityX(50);
} else if (this.gameState.cursors.up.isDown) {
this.player.setVelocityY(-50);
} else if (this.gameState.cursors.down.isDown) {
this.player.setVelocityY(-50);
} else {
this.player.setVelocityX(0);
}
this.updateMap();
}
updateMap() {
console.log(this.gameState);
var origin = this.gameState.map.getTileAtWorldXY(
this.player.x,
this.player.y
);
this.gameState.map.forEachTile(function (tile) {
var dist = Phaser.Math.Distance.Chebyshev(
origin.x,
origin.y,
tile.x,
tile.y
);
tile.setAlpha(1 - 0.1 * dist);
}, this);
}
}
Since you are moving the player with physics velocity, the player could be outside of the map, or on a position without a tile.
When map.getTileAtWorldXY is called with a position outside of the map (or some other invalid position), or there is no tile placed in that position on the layer, this function will return null, that's probably why you get this specific error.
(link to the documentation)
The easy solution would be just to exit the function, when the origin equals null. Like this:
updateMap() {
var origin = this.gameState.map.getTileAtWorldXY(
this.player.x,
this.player.y
);
if(!origin){
return;
}
...
}
Or you could have a variable like: this.gameState.lastValidPosition, that you set when the player has a valid position, and use this variable, in case of an error
updateMap() {
var origin = this.gameState.map.getTileAtWorldXY(
this.player.x,
this.player.y
);
if(!origin && !this.gameState.lastValidPosition ){
// origin is invalid and we have no valid last position
return;
} else if( !origin ) { // origin is null, w use the last valid position
origin = this.gameState.lastValidPosition;
} else { // origin is not null, so we save this position
this.gameState.lastValidPosition = origin;
}
...
}
Both solution will produce the exact same result (for most of the usecases). Which os keeping the light source at the last known/valid position.
Update:
a more easy solution could be to use the third parameter nonnull, of the function map.getTileAtWorldXY (if the issue is a missing tile), this would return a tile with index -1 instead of null.
like:
var origin = this.gameState.map.getTileAtWorldXY(
this.player.x,
this.player.y,
true
);

How to invoke a function when a class is instantiated

I have two classes, one of which is a subclass of the other. What I want to do is have one of the functions, step, invoked when the subclass is instantiated. This will cause the subclass to blink. I can see how to do it with functional instantiation but not with pseudoclassical. I've attached the jquery and javascript. Basically, when the button is clicked, I want the blinky dancer to blink, this should happen via the step function. Maybe I'm calling it incorrectly in the dancer superclass?
$(document).ready(function() {
window.dancers = [];
$('.addDancerButton').on('click', function(event) {
var dancerMakerFunctionName = $(this).data('dancer-maker-function-name');
console.log(dancerMakerFunctionName);
var dancerMakerFunction = window[dancerMakerFunctionName];
var dancer = new dancerMakerFunction(
$("body").height() * Math.random(),
$("body").width() * Math.random(),
Math.random() * 1000
);
$('body').append(dancer.$node);
});
});
var makeBlinkyDancer = function(top, left, timeBetweenSteps) {
makeDancer.call(this, top, left, timeBetweenSteps);
};
makeBlinkyDancer.prototype = Object.create(makeDancer.prototype);
makeBlinkyDancer.prototype.constructor = makeBlinkyDancer;
var oldStep = makeBlinkyDancer.prototype.step;
makeBlinkyDancer.prototype.step = function() {
oldStep();
this.$node.toggle();
};
var makeDancer = function(top, left, timeBetweenSteps) {
this.$node = $('<span class="dancer"></span>');
};
makeDancer.prototype.step = function() {
setTimeout(step.bind(this), timeBetweenSteps);
};
makeDancer.prototype.setPosition = function (top, left) {
var styleSettings = {
top: top,
left: left
};
this.$node.css(styleSettings);
};
// makeDancer.prototype.setPosition();
You can call the method when the subclass is instantiated by calling init in the constructor. In the example and demo I called this method makeBlink rather than step or oldStep. Hope that makes it clear.
// the subclass BlinkyDancer
var BlinkyDancer = function (top, left, height, width, timeBetweenSteps) {
// ... get props from parent class
// make blink when sublcass instantiated
this.init = this.makeBlink();
};
Full Demo/Solution
Demo: https://codesandbox.io/s/call-method-when-class-instantiated-mnesxt
HTML
<body>
<div id="app">
<button id="make-blink">Make Blink</button>
<button id="stop-blink">Stop Blink</button>
</div>
</body>
JS
import $ from "jquery";
var Dancer = function (top, left, height, width) {
this.$node = $('<div class="dancer"></div>');
this.top = top;
this.left = left;
this.height = height;
this.width = width;
this.bg = "blue";
};
Dancer.prototype.putDancerOnScreen = function () {
this.$node.height(this.height).width(this.width);
this.$node.css("background-color", "blue");
this.$node.css("position", "absolute");
$("body").append(this.$node);
};
Dancer.prototype.setPosition = function () {
this.$node.css("top", this.top);
this.$node.css("left", this.left);
};
var myDancer = new Dancer(
Math.floor(Math.random() * 500),
Math.floor(Math.random() * 500),
50,
50
);
myDancer.putDancerOnScreen();
myDancer.setPosition();
// the subclass BlinkyDancer
var BlinkyDancer = function (top, left, height, width, timeBetweenSteps) {
Dancer.call(this, top, left, height, width); // get the props set up by Dancer
this.timeBetweenSteps = timeBetweenSteps;
this.blinkerId = null;
// make blink when sublcass instantiated
this.init = this.makeBlink();
};
// set inheritance and Dancer constructor function
BlinkyDancer.prototype = Object.create(Dancer.prototype);
BlinkyDancer.prototype.constructor = Dancer;
// add subclass methods
BlinkyDancer.prototype.makeBlink = function () {
if (this.blinkerId !== null) {
return;
}
let count = 0;
let blinkerId = setInterval(() => {
// do whatever thing you want to indicate blinking/dancing
if (count % 2 === 0) {
this.$node.css("background-color", "red");
} else {
this.$node.css("background-color", "blue");
}
count++;
}, this.timeBetweenSteps);
this.blinkerId = blinkerId;
console.log("blinkder id set: ", this.blinkerId);
};
BlinkyDancer.prototype.stopBlink = function () {
if (this.blinkerId === null) {
// already blinking
return;
}
if (this.blinkerId !== null) {
clearInterval(this.blinkerId);
console.log("blink id cleared: ", this.blinkerId);
this.blinkerId = null;
}
};
// instantiate a new subclass
let myBlinkyDancer = new BlinkyDancer(
Math.floor(Math.random() * 500),
Math.floor(Math.random() * 500),
50,
50,
25
);
// use parent class methods to put the element on screen
myBlinkyDancer.putDancerOnScreen();
myBlinkyDancer.setPosition();
const makeBlinkButton = document.getElementById("make-blink");
const stopBlinkButton = document.getElementById("stop-blink");
makeBlinkButton.addEventListener("click", function () {
myBlinkyDancer.makeBlink();
});
stopBlinkButton.addEventListener("click", function () {
myBlinkyDancer.stopBlink();
});
Explanation
The Dancer class takes some props related to positioning and dimensions. It gets a blue background and has two methods for adding the element to the DOM and setting the position on screen.
BlinkyDancer is a subclass that inherits all the props of Dancer as well as two new props and two new methods.
What I want to do is have one of the functions, step, invoked when the
subclass is instantiated.
When a new BlinkyDancer is instantiated, we call makeBlink right away with init so the element starts blinking.
// make blink when sublcass instantiated
this.init = this.makeBlink();
I used an alternating background color to demonstrate the blinking effect.
The makeBlink and stopBlink methods work in tandem with the new props timeBetweenSteps and blinkerId.
when the button is clicked, I want the blinky dancer to blink, this
should happen via the step function.
Buttons are wired up to trigger starting and stopping the blinking effect. I've replaced step in your example with simple and declarative methods for making the blink and stopping it.
makeBlinkButton.addEventListener("click", function () {
myBlinkyDancer.makeBlink();
});
The blinking effect uses setInterval to toggle the background color using the timeBetweenSteps prop as the interval duration. startBlink starts the interval and sets blinkerId to the corresponding interval ID. To stop the blinking effect, clearInterval is passed the blinkerId to clear it (stopping the blinking effect) before resetting it to null.
BlinkyDancer.prototype.makeBlink = function () {
if (this.blinkerId !== null) {
return;
}
let count = 0;
let blinkerId = setInterval(() => {
// do whatever thing you want to indicate blinking/dancing
if (count % 2 === 0) {
this.$node.css("background-color", "red");
} else {
this.$node.css("background-color", "blue");
}
count++;
}, this.timeBetweenSteps);
this.blinkerId = blinkerId;
console.log("blinkder id set: ", this.blinkerId);
};
BlinkyDancer.prototype.stopBlink = function () {
if (this.blinkerId === null) {
// already blinking
return;
}
if (this.blinkerId !== null) {
clearInterval(this.blinkerId);
console.log("blink id cleared", this.blinkerId);
this.blinkerId = null;
}
};
Maybe I'm calling [step] incorrectly in the dancer superclass?
It looks like you were on the right track but missing parens to call the method. In your example code, rather than:
makeBlinkyDancer.prototype.step;
You would do:
makeBlinkyDancer.prototype.step();

Phaser 2 determining which unit is active

I have 3 clickable objects. When one is clicked, this becomes the 'selected unit'.
I am trying to create some generic actions for the units such as moving to a point.
In my create function I initialize the units, when a unit is clicked on - this is supposed to become the 'selected unit' so that my movement and direction function applies to the this unit. However, the script is not able to recognize which unit intend for example I get this error:
Uncaught TypeError: Cannot read property 'velocity' of undefined.
Is there a way to use a variable to indicate selected users and pass that to the functions?
window.onload = function() {
var block_count = 0;
var block = '';
var selected_unit = '';
var unit_clicked = 0;
var tank1 = null;
var game = new Phaser.Game(800, 600, Phaser.AUTO, '', { preload: preload, create: create, update: update, render: render});
function preload () {
game.load.image('block', 'block.png');
game.load.image('tank1', 'tank.png');
game.load.image('baddie', 'tank.png');
game.load.image('mouse_btn', 'block.png');
game.input.mouse.capture = true;
}
function create () {
game.physics.startSystem(Phaser.Physics.ARCADE);
mouse_btn = game.add.sprite(30,30, 'mouse_btn');
mouse_btn.anchor.setTo(0.5, 0.5);
//T1
tank1 = game.add.sprite(30,30, 'tank1');
initialise_player(tank1);
game.physics.enable(tank1, Phaser.Physics.ARCADE);
//T2
tank2 = game.add.sprite(30,60, 'tank1');
initialise_player(tank2);
game.physics.enable(tank2, Phaser.Physics.ARCADE);
game.world.setBounds(0, 0, 2000, 2000);
game.camera.follow(tank1);
}
function update () {
if(selected_unit == '') {
mouse_btn.x = game.input.mousePointer.worldX
mouse_btn.y = game.input.mousePointer.worldY
}
if(game.input.activePointer.leftButton.isDown && block_count == 0 && unit_clicked == 0) {
game.input.activePointer.leftButton.stop(event);
block_count =1;
block = game.add.sprite(game.input.mousePointer.worldX, game.input.mousePointer.worldY, 'block');
game.physics.enable(block, Phaser.Physics.ARCADE);
block.anchor.setTo(0.5, 0.5)
lookAtObject(selected_unit, block, 0.005);
}
if(block.alive){
game.physics.arcade.moveToObject(selected_unit, block, 260, 0)
} else {
console.log(selected_unit)
selected_unit.body.velocity.x = 0;
selected_unit.body.velocity.y = 0;
}
if(game.physics.arcade.collide(selected_unit, block)) {
block_count--;
block.kill();
}
}
function render(){
//console.log(game.physics.arcade.collide(tank1, block))
}
function lookAtObject(obj, target, rotspeed){
var angle = Math.atan2(block.y - tank1.y, block.x - tank1.body.x);
tank1.rotation = angle + game.math.degToRad(90);
}
function initialise_player(tank1){
tank1.anchor.setTo(0.5, 0.5);
tank1.inputEnabled = true;
tank1.input.useHandCursor = true;
tank1.events.onInputDown.add(t1_clicked,this);
tank1.events.onInputOver.add(t1_over, this)
tank1.events.onInputOut.add(t1_out, this)
}
function t1_clicked() {
selected_unit = tank1;
}
function t1_over() {
unit_clicked = 1
}
function t1_out () {
unit_clicked = 0
}
};
The error you're getting on initial load is because in update you're making an assumption that selected_unit exists and has a body.
Update your third if to make sure selected_unit is defined.
if (selected_unit !== '') {
selected_unit.body.velocity.x = 0;
selected_unit.body.velocity.y = 0;
}
However, a better option would be to put this a few lines down, where you kill the block instead.
if(game.physics.arcade.collide(selected_unit, block)) {
selected_unit.body.velocity.x = 0;
selected_unit.body.velocity.y = 0;
block_count--;
block.kill();
}
if (block.alive); moveToObject is also expecting selected_unit to exist and have a body, which may not be the case; wrap it with a check.
if (selected_unit !== '') {
game.physics.arcade.moveToObject(selected_unit, block, 260, 0)
}
That now allows tank1 to rotate to look at the item you just placed, but it doesn't move it until it or tank2 have been clicked on.
This also points out that there are a number of tweaks you'll want to make to your code in general, since you're ignoring arguments that are being passed in. For example, t1_clicked isn't using the sprite that's been clicked on, but is instead just hard-coding tank1. lookAtObject isn't using obj or target, but again has values hard-coded in.
One other thing you may want to change is the following:
if(selected_unit == '') {
mouse_btn.x = game.input.mousePointer.worldX
mouse_btn.y = game.input.mousePointer.worldY
}
If you make that the following, you won't end up with an extra sprite hanging about on the screen.
if (block_count === 0) {
mouse_btn.x = game.input.mousePointer.worldX;
mouse_btn.y = game.input.mousePointer.worldY;
}

Run a function when animation completes using Curtains.js

I'm looking to call a function right at the end of the wobble effect.
That is, at the end of the damping effect (when the wobble stops), I'd like to execute a GSAP timeline function. I'd assume this type of "onComplete" function would need to be called inside the onReady() of Curtains and perhaps by tracking the damping effect. I'm only familiar with GSAP's onComplete function, but don't know how I would implement it here. Maybe something that checks if deltas.applied is less than 0.001, then the function is called?
Below is the code snippet (without the fragment and vertex shaders). Full working code here:
CodePen
class Img {
constructor() {
const curtain = new Curtains({
container: "canvas",
watchScroll: false,
});
const params = {
vertexShader,
fragmentShader,
uniforms: {
time: {
name: "uTime",
type: "1f",
value: 0,
},
prog: {
name: "uProg",
type: "1f",
value: 0,
}
}
}
const planeElements = document.getElementsByClassName("plane")[0];
this.plane = curtain.addPlane(planeElements, params);
if (this.plane) {
this.plane
.onReady(() => {
this.introAnim();
})
.onRender(() => {
this.plane.uniforms.time.value++;
deltas.applied += (deltas.max - deltas.applied) * 0.05;
deltas.max += (0 - deltas.max) * 0.07;
this.plane.uniforms.prog.value = deltas.applied
})
}
// error handling
curtain.onError(function() {
document.body.classList.add("no-curtains");
});
}
introAnim() {
deltas.max = 6;
//console.log("complete") <-- need an onComplete type function~!
}
}
window.onload = function() {
const img = new Img();
}
What you could use is some algebra :)
First off, you should simplify your deltas.max function like so:
deltas.max += (0 - deltas.max) * 0.07;
// Simplifies to
deltas.max -= deltas.max * 0.07;
// Rewrite to
deltas.max = deltas.max - deltas.max * 0.07;
// Rewrite to
deltas.max = deltas.max * (1 - 0.07);
// Simplifies to
deltas.max *= 0.93; // Much nicer :)
That is actually pretty important to do because it makes our work of calculating the end value of our time variable and the duration of our animation significantly Easier:
// Given deltas.max *= 0.93, need to calculate end time value
// endVal = startVal * reductionFactor^n
// Rewrite as
// n = ln(endVal / startVal) / ln(reductionFactor) // for more see https://www.purplemath.com/modules/solvexpo2.htm
// n = ln(0.001 / 8) / ln(0.93)
const n = 123.84;
// Assuming 60fps normally: n / 60
const dur = 2.064;
Once we have those values all we have to do is create a linear tween animating our time to that value with that duration and update the max and prog values in the onUpdate:
gsap.to(this.plane.uniforms.time, {
value: n,
duration: dur,
ease: "none",
onUpdate: () => {
this.deltas.applied += (this.deltas.max - this.deltas.applied) * 0.05;
this.deltas.max *= 0.93;
this.plane.uniforms.prog.value = this.deltas.applied;
},
onComplete: () => console.log("complete!")
});
Then you get "complete!" when the animation finishes!
To make sure that your Curtains animations run at the proper rate even with monitors with high refresh rates (even the ones not directly animated with GSAP) it's also a good idea to turn off Curtain's autoRendering and use GSAP's ticker instead:
const curtains = new Curtains({ container: "canvas", autoRender: false });
// Use a single rAF for both GSAP and Curtains
function renderScene() {
curtains.render();
}
gsap.ticker.add(renderScene);
Altogether you get this demo.
This won't be the best answer possible but you can take some ideas and insights from it.
Open the console and see that when the animation gets completed it gets fired only once.
//Fire an onComplete event and listen for that
const event = new Event('onComplete');
class Img {
constructor() {
// Added a instance variable for encapsulation
this.animComplete = {anim1: false}
//Changed code above
const curtain = new Curtains({
container: "canvas",
watchScroll: false,
});
const params = {
vertexShader,
fragmentShader,
uniforms: {
time: {
name: "uTime",
type: "1f",
value: 0,
},
prog: {
name: "uProg",
type: "1f",
value: 0,
}
}
}
const planeElements = document.getElementsByClassName("plane")[0];
this.plane = curtain.addPlane(planeElements, params);
if (this.plane) {
this.plane
.onReady(() => {
this.introAnim();
document.addEventListener('onComplete', ()=>{
//Do damping effects here
console.log('complete')
})
})
.onRender(() => {
this.plane.uniforms.time.value++;
deltas.applied += (deltas.max - deltas.applied) * 0.05;
deltas.max += (0 - deltas.max) * 0.07;
this.plane.uniforms.prog.value = deltas.applied
if(deltas.applied<0.001 && !this.animComplete.anim1){
document.dispatchEvent(event)
this.animComplete.anim1 = true
}
})
}
// error handling
curtain.onError(function() {
document.body.classList.add("no-curtains");
});
}
introAnim() {
deltas.max = 6;
}
}
window.onload = function() {
const img = new Img();
}
I've found a solution to call a function at the end of the damping (wobble) effect, that doesn't use GSAP, but uses the Curtains onRender method. Because the uTime value goes up infinitely and the uProg value approaches 0, By tracking both the uTime and uProg values inside the Curtains onRender method we can find a point (2 thresholds) at which the damping effect has essentially completed. Not sure if this is the most efficient way, but it seems to work.
.onRender(() => {
if (this.plane.uniforms.prog.value < 0.008 && this.plane.uniforms.time.value > 50) { console.log("complete")}
})
Thanks to the Curtains docs re asynchronous-textures, I was able to better control the timing of the wobble effect with the desired result every time. That is, on computers with lower FPS, the entire damping effect takes place smoothly, with an onComplete function called at the end, as well as on comps with higher frame rates.
Although, as mentioned there is less control over the length of the effect, as we are not using GSAP to control the Utime values. Thanks #Zach! However, using a "threshold check" inside the curtains onRender this way, means the damping wobble effect is never compromised, if we were to disable the drawing at the on complete call.
By enabling the drawing at the same time the image is loaded we avoid any erratic behaviour. The following works now with hard refresh as well.
export default class Img {
constructor() {
this.deltas = {
max: 0,
applied: 0,
};
this.curtain = new Curtains({
container: "canvas",
watchScroll: false,
pixelRatio: Math.min(1.5, window.devicePixelRatio),
});
this.params = {
vertexShader,
fragmentShader,
uniforms: {
time: {
name: "uTime",
type: "1f",
value: 0,
},
prog: {
name: "uProg",
type: "1f",
value: 0,
},
},
};
this.planeElements = document.getElementsByClassName("plane")[0];
this.curtain.onError(() => document.body.classList.add("no-curtains"));
this.curtain.disableDrawing(); // disable drawing to begin with to prevent erratic timing issues
this.init();
}
init() {
this.plane = new Plane(this.curtain, this.planeElements, this.params);
this.playWobble();
}
loaded() {
return new Promise((resolve) => {
// load image and enable drawing as soon as it's ready
const asyncImgElements = document
.getElementById("async-textures-wrapper")
.getElementsByTagName("img");
// track image loading
let imagesLoaded = 0;
const imagesToLoad = asyncImgElements.length;
// load the images
this.plane.loadImages(asyncImgElements, {
// textures options
// improve texture rendering on small screens with LINEAR_MIPMAP_NEAREST minFilter
minFilter: this.curtain.gl.LINEAR_MIPMAP_NEAREST,
});
this.plane.onLoading(() => {
imagesLoaded++;
if (imagesLoaded === imagesToLoad) {
console.log("loaded");
// everything is ready, we need to render at least one frame
this.curtain.needRender();
// if window has been resized between plane creation and image loading, we need to trigger a resize
this.plane.resize();
// show our plane now
this.plane.visible = true;
this.curtain.enableDrawing();
resolve();
}
});
});
}
playWobble() {
if (this.plane) {
this.plane
.onReady(() => {
this.deltas.max = 7; // 7
})
.onRender(() => {
this.plane.uniforms.time.value++;
this.deltas.applied += (this.deltas.max - this.deltas.applied) * 0.05;
this.deltas.max += (0 - this.deltas.max) * 0.07;
this.plane.uniforms.prog.value = this.deltas.applied;
console.log(this.plane.uniforms.prog.value);
// ---- "on complete" working!! ( even on hard refresh) -----//
if (
this.plane.uniforms.prog.value < 0.001 &&
this.plane.uniforms.time.value > 50
) {
console.log("complete");
this.curtain.disableDrawing();
}
});
}
}
destroy() {
if (this.plane) {
this.curtain.disableDrawing();
this.curtain.dispose();
this.plane.remove();
}
}
}
const img = new Img();
Promise.all([img.loaded()]).then(() => {
console.log("animation started");
});

JavaScript game switchable sprites

Any ideas how to make switchable characters I have a html game it's finished but I want to implement a way to switch my main character.
Simple coding using Phaser framework
upload function() {
this.game.load.sprite ("bird" assets/bird.png);
this.game.load.sprite ("bird2" assets/bird2.png);
this.game.load.sprite ("bird3" assets/bird3.png);
},
create function() {
this.game.add.sprite (0, 0 "bird" );
},
I want to be able to switch my playable character the "bird" with the "bird2" or "bird3" through a selection button if a player selects a switch character button for the playable character to switch to that. I'm pretty sure this is something simple but I'm still pretty new with coding.
I want a button where I press then I can switch the character
(Button 1) switches to bird2
"if button 1 is selected button two and current bird are disabled"-only bird2 is visible
(Button 2) switches to bird3
"if button 2 is selected button one and current bird are disabled"-only bird3 is visible
Edit This is My current code and states
var MainState = {
//load the game assets before the game starts
preload: function () {
this.load.image('background', 'assets/spring2.png');
this.load.spritesheet('bird', 'assets/bird.png',52 ,28, 7);
this.load.spritesheet('bird2', 'assets/bird2.png',52 ,28, 7);
this.load.spritesheet('bird3', 'assets/bird3.png',52 ,28, 7);
this.load.image('pipe', 'assets/pipe4.png');
},
//executed after everything is loaded
create: function () {
this.background = game.add.tileSprite(0, game.height-736,game.width, 736, 'background');
this.background.autoScroll(-20,0);
/////Bird///////////////////////////////////////////////////
this.bird = this.game.add.sprite(100, 200, 'bird');
this.bird.animations.add('fly');
this.bird.animations.play('fly', 50, true);
game.physics.startSystem(Phaser.Physics.ARCADE);
game.physics.arcade.enable(this.bird);
this.bird.body.gravity.y = 1000;
var spaceKey = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
this.bird.body.collideWorldBounds=true;
this.bird.body.immovable= true;
game.input.onDown.add(this.jump, this); //////touch screen jump
spaceKey.onDown.add(this.jump, this);
///////////////////////////////////////////////////////Pipes
this.pipes = game.add.group();
//timer
this.timer = game.time.events.loop(1600, this.addRowOfPipes, this); /////////////timer for pipes
///Bird anchor
this.bird.anchor.setTo(-0.2, 0.5)
},
// this is execated multiple times per second
update: function () {
if (this.bird.y < 0 || this.bird.y > 480)
game.state.start("StateOver");
///Collision
game.physics.arcade.overlap(
this.bird, this.pipes, this.restartGame, null, this);
///Bird Angle
if (this.bird.angle < 30)
this.bird.angle += 1;
///////////////music stop w top+bottom collision
if (this.bird.y < 0 || this.bird.y > 479)
music.stop();
},
jump: function () {
//this is for so the bird wount fly once dead
if (this.bird.alive == false)
return;
// Add a vertical velocity to the bird
this.bird.body.velocity.y = -350;
// Jump Animation
var animation = game.add.tween(this.bird);
// Change the angle of the bird to -20° in 100 milliseconds
animation.to({angle: -20}, 100);
// And start the animation
animation.start();
game.add.tween(this.bird).to({angle: -20}, 100).start();
},
restartGame: function () {
// Start the 'main' state, which restarts the game
game.state.start(game.state.StateOver); /////////////////////changed from current #########
///Hit pipe Null
game.physics.arcade.overlap(
this.bird, this.pipes, this.hitPipe, null, this);
},
addRowOfPipes: function() {
var hole = Math.floor(Math.random() * 5) + 1; ///Math.floor(Math.random() * 5) + 1;
for (var i = 0; i < 10 ; i++) ///// (var i = 0; i < 8; i++)
if (i != hole && i != hole + 1) ///// if (i != hole && i != hole + 1)
this.addOnePipe(440, i * 50 ); ///// 640 starting point of pipe 240 point of down ////this.addOnePipe(480, i * 60 + 10);
},
addOnePipe: function(x, y) {
var pipe = game.add.sprite(x, y, 'pipe');
this.pipes.add(pipe);
game.physics.arcade.enable(pipe);
pipe.body.velocity.x = -200;
pipe.checkWorldBounds = true;
pipe.outOfBoundsKill = true;
},
hitPipe: function() {
// If the bird has already hit a pipe, do nothing
// It means the bird is already falling off the screen
if (this.bird.alive == false)
return;
else {
game.state.start("StateOver");
}
// Set the alive property of the bird to false
this.bird.alive = false;
// Prevent new pipes from appearing
game.time.events.remove(this.timer);
// Go through all the pipes, and stop their movement
this.pipes.forEach(function(p){
p.body.velocity.x = 0;
}, this);
},
};
character.js
var characters={
preload:function()
{
game.load.spritesheet('button', 'assets/button.png', 215, 53, 8);
game.load.image("background", "assets/characterbackground.png");
game.load.image("pajaro", "assets/storeicon.png");
game.load.image("logo", "assets/extra/storef.png");
this.load.spritesheet('bird', 'assets/bird.png',52 ,28, 7);
this.load.spritesheet('bird2', 'assets/bird2.png',52 ,28, 7);
this.load.spritesheet('bird3', 'assets/bird3.png',52 ,28, 7);
game.load.spritesheet("button2", 'assets/button2.png', 100, 10, 10);
},
create:function()
{
bird = game.add.image(140, 150, 'pajaro');
logo = game.add.image (20, 350, 'logo');
this.background = game.add.tileSprite(0, game.height-736,game.width, 736, 'background');
this.background.autoScroll(-100,0);
this.btnMainMenu=game.add.button(130,500,'button',this.mainMenu,this,4,5,4);
this.btnbird=game.add.button(180,600,"button2",this.changebird2,this,0,1,0);
},
mainMenu:function()
{
game.state.start("stateTitle");
},
update:function()
{
// bird.x +=1;
},
changebird2: function(){
},
};
Instead of creating three sprites that you either hide or show, I might recommend just changing what texture is loaded when the sprite is created/added.
To do this you'll need to store a reference to the playable character, which you probably already have.
// On the game itself, add a reference.
this.bird = null;
// In your preload, load the different images.
this.load.image('bird', 'assets/bird.png');
this.load.image('bird2', 'assets/bird2.png');
this.load.image('bird3', 'assets/bird3.png');
// When creating, default to one.
this.bird = this.game.add.sprite(0, 0, 'bird');
// In your function where they select a new skin, you can load in a different texture.
this.bird.loadTexture('bird3');
Alternatively, you could store the key that should be used on the game.
// On the game itself, track which key to use.
this.birdSkin = 'bird';
// You'll still have to load your possible textures.
this.load.image('bird', 'assets/bird.png');
this.load.image('bird2', 'assets/bird2.png');
this.load.image('bird3', 'assets/bird3.png');
// Now when creating just use the variable.
this.bird.loadTexture(this.birdSkin);
The Phaser init() will allow 0 or more parameters to be passed in (see the end of Phaser Tutorial: understanding Phaser states), which is where you could populate this.birdSkin.
I would look at what states you're using to determine what's best for you. If you have one state for the game and another for selecting which image/texture is used, than the second option might be better.
Update for Character State
Given your comments and what I saw in your code, I created a short example that you could tweak for your use.
There's a JSFiddle available, but the code is also included below.
var mainState = {
preload: function() {
// Load the three sprites that they can choose between.
this.load.crossOrigin = 'anonymous';
this.load.image('ball', 'https://raw.githubusercontent.com/photonstorm/phaser-examples/master/examples/assets/sprites/orb-blue.png');
this.load.image('ball2', 'https://raw.githubusercontent.com/photonstorm/phaser-examples/master/examples/assets/sprites/orb-green.png');
this.load.image('ball3', 'https://raw.githubusercontent.com/photonstorm/phaser-examples/master/examples/assets/sprites/orb-red.png');
},
create: function() {
this.ball = this.game.add.sprite(this.game.world.centerX, this.game.world.centerY, this.game.global.skin);
this.ball.anchor.setTo(0.5);
// Let the ball be acted upon. This will allow the player to change the sprite used.
this.ball.inputEnabled = true;
this.ball.events.onInputDown.add(this.changeCharacter, this);
},
update: function() {
},
changeCharacter: function() {
game.state.start('character');
}
};
var characterState = {
preload: function() {
},
create: function() {
// For this, add our three possible ball skins.
this.ball1 = this.game.add.sprite(this.game.world.centerX, this.game.world.centerY / 2, 'ball');
this.ball1.anchor.setTo(0.5);
this.ball1.inputEnabled = true;
this.ball2 = this.game.add.sprite(this.game.world.centerX, this.game.world.centerY, 'ball2');
this.ball2.anchor.setTo(0.5);
this.ball2.inputEnabled = true;
this.ball3 = this.game.add.sprite(this.game.world.centerX, this.game.world.centerY * 1.5, 'ball3');
this.ball3.anchor.setTo(0.5);
this.ball3.inputEnabled = true;
// Use the selected ball's sprite in our main game.
this.ball1.events.onInputDown.add(this.selectBall, this);
this.ball2.events.onInputDown.add(this.selectBall, this);
this.ball3.events.onInputDown.add(this.selectBall, this);
},
update: function() {
},
selectBall: function(sprite, pointer) {
// Grab the key of the sprite and save it to our global variable.
this.game.global.skin = sprite.key;
this.game.state.start('main');
}
};
var game = new Phaser.Game(200, 200);
// Create a global object that we can add custom variables to.
game.global = {
skin: 'ball'
};
game.state.add('main', mainState);
game.state.add('character', characterState);
game.state.start('main');
This actually simplifies things a bit, in that it just uses a global variable (I've been using TypeScript the last handful of months, so there's probably a better way to declare this).

Categories

Resources