HyperCrowd / moba

Quick MOBA Concept using Phaser.js
0 stars 0 forks source link

Visual Effects System #19

Open HyperCrowd opened 1 month ago

HyperCrowd commented 1 month ago
HyperCrowd commented 4 weeks ago

To create a temporary shape, emitter, and shader pipeline in Phaser and have them automatically destroy after a set duration, you can use Phaser’s time.delayedCall function. This method will allow each effect to be created, displayed, and then removed after the duration expires. Here’s an example demonstrating this approach:

function createTemporaryEffects(gameObject, duration) {
    // 1. Create a shape (e.g., a circle) around the game object
    const graphics = this.add.graphics();
    graphics.lineStyle(2, 0xff0000, 1); // Set color and line thickness
    graphics.strokeCircle(gameObject.x, gameObject.y, gameObject.width * 0.6); // Draw circle

    // Update the shape position if the object moves
    this.events.on('update', () => {
        graphics.x = gameObject.x;
        graphics.y = gameObject.y;
    });

    // 2. Create a particle emitter that follows the game object
    const particles = this.add.particles('particleTexture');
    const emitter = particles.createEmitter({
        speed: { min: 200, max: 400 },
        scale: { start: 0.5, end: 0 },
        blendMode: 'ADD'
    });
    emitter.startFollow(gameObject);

    // 3. Apply a shader pipeline to the game object
    const shaderPipeline = this.game.renderer.pipelines.get('CustomShader') || this.game.renderer.addPipeline('CustomShader', new Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline({
        game: this.game,
        renderer: this.game.renderer,
        fragShader: `
            precision mediump float;
            uniform float time;
            uniform vec2 resolution;
            void main() {
                vec2 uv = gl_FragCoord.xy / resolution.xy;
                gl_FragColor = vec4(uv.x * sin(time), uv.y * cos(time), 0.5, 1.0);
            }
        `
    }));
    shaderPipeline.setFloat2('resolution', this.game.config.width, this.game.config.height);
    gameObject.setPipeline('CustomShader');

    // Animate the shader over time
    this.events.on('update', (time, delta) => {
        shaderPipeline.setFloat1('time', time / 1000); // Progresses time for shader animation
    });

    // 4. Schedule destruction after the duration
    this.time.delayedCall(duration, () => {
        // Remove the graphics shape
        graphics.destroy();

        // Stop and remove the particle emitter
        emitter.stop();
        particles.destroy();

        // Remove the shader pipeline
        gameObject.resetPipeline();

        // Optionally remove the shader from the renderer if you’re done with it
        this.game.renderer.removePipeline('CustomShader');
    });
}

// Usage: Attach effects to `myGameObject` for 3000 ms (3 seconds)
createTemporaryEffects.call(this, myGameObject, 3000);
HyperCrowd commented 4 weeks ago

In Phaser, you don’t need to create a separate particle system for each entity. You can create a single Particles instance and then use multiple emitters from that instance to follow different entities. This approach is efficient and keeps your codebase cleaner since all emitters are centralized under one particle system.

Here’s how you can achieve this:

Here's how the code would look:

function attachParticleEffectToEntity(particles, gameObject) {
    // Create an emitter for the specific gameObject
    const emitter = particles.createEmitter({
        speed: { min: 200, max: 400 },
        scale: { start: 0.5, end: 0 },
        blendMode: 'ADD'
    });

    // Attach the emitter to follow the gameObject
    emitter.startFollow(gameObject);

    return emitter;
}

// Create a single particles instance
const particles = this.add.particles('particleTexture');

// Use the same particles instance to create emitters for multiple entities
const emitter1 = attachParticleEffectToEntity.call(this, particles, entity1);
const emitter2 = attachParticleEffectToEntity.call(this, particles, entity2);
const emitter3 = attachParticleEffectToEntity.call(this, particles, entity3);

// Schedule cleanup after a specific duration for each emitter (e.g., 3 seconds)
this.time.delayedCall(3000, () => {
    emitter1.stop();
    emitter2.stop();
    emitter3.stop();
});
HyperCrowd commented 4 weeks ago

For shader pipelines (renderer pipelines), you can reuse the same pipeline for multiple game objects in Phaser. You don’t need to create a new pipeline instance for each object; instead, you can set the same pipeline on different objects and update shared uniforms if needed. Phaser will automatically apply the shader to all objects using the pipeline.

Here’s how you can set up a single pipeline and apply it to multiple game objects, with cleanup after a set duration:

// Step 1: Create a single custom shader pipeline if it hasn’t been added yet
const shaderCode = `
    precision mediump float;
    uniform float time;
    uniform vec2 resolution;
    void main() {
        vec2 uv = gl_FragCoord.xy / resolution.xy;
        gl_FragColor = vec4(uv.x * sin(time), uv.y * cos(time), 0.5, 1.0);
    }
`;

if (!this.game.renderer.pipelines.get('CustomShader')) {
    const customPipeline = this.game.renderer.addPipeline('CustomShader', new Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline({
        game: this.game,
        renderer: this.game.renderer,
        fragShader: shaderCode
    }));
    customPipeline.setFloat2('resolution', this.game.config.width, this.game.config.height);
}

// Step 2: Apply the pipeline to multiple game objects
gameObject1.setPipeline('CustomShader');
gameObject2.setPipeline('CustomShader');
gameObject3.setPipeline('CustomShader');

// Update time uniform for animation across all objects using the pipeline
this.events.on('update', (time) => {
    const customPipeline = this.game.renderer.pipelines.get('CustomShader');
    if (customPipeline) {
        customPipeline.setFloat1('time', time / 1000);
    }
});

// Step 3: Schedule pipeline removal for each object after a duration
const duration = 3000; // e.g., 3 seconds
this.time.delayedCall(duration, () => {
    gameObject1.resetPipeline();
    gameObject2.resetPipeline();
    gameObject3.resetPipeline();

    // Optionally remove the pipeline from the renderer if it’s no longer needed
    this.game.renderer.removePipeline('CustomShader');
});
HyperCrowd commented 3 weeks ago
HyperCrowd commented 3 weeks ago

Gotta use sprites of shapes instead :/

https://docs.phaser.io/phaser/concepts/gameobjects/graphics#generate-texture

When a Graphics object is rendered it will render differently based on if the game is running under Canvas or WebGL. Under Canvas it will use the HTML Canvas context drawing operations to draw the path. Under WebGL the graphics data is decomposed into polygons. Both of these are expensive processes, especially with complex shapes.

If your Graphics object doesn't change much (or at all) once you've drawn your shape to it, then you will help performance by calling Phaser.GameObjects.Graphics#generateTexture. This will 'bake' the Graphics object into a Texture, and return it. You can then use this Texture for Sprites or other display objects. If your Graphics object updates frequently then you should avoid doing this, as it will constantly generate new textures, which will consume memory.

As you can tell, Graphics objects are a bit of a trade-off. While they are extremely useful, you need to be careful in their complexity and quantity of them in your game.