How to make 2D collapsing block-tiles with physics

Hello,

For the 2D tile-based game that I’m working on, when collapsing a block-tile I wanted that the block would break using a realistic 2D physics effect, where the block will collapse into small pieces and each piece to be able to collide naturally with the surrounding areas while falling down to gravity.

I’ve tried to play around with the ParticleSystem, but I couldn’t find how to have a per particle different sprite, plus how to make each particle to be collidable.

Researching the physics of other 2D games I’ve found BROFORCE, and though is a completely different concept from my game, it has the desired 2d block collapsing physics.
I’m placing a youtube link of the gameplay so you can see how the blocks destroy into particles.

Any recommendations on how to achieve such effect in 2D?
Also, is there any asset in the store which would help me to reach such effect?

Thanks

Your first thought is correct, this effect needs a particle system. You destroy the block, then spawn a particle emission at the same location to give this effect.

The Unity particle system seems insufficient for your needs at the moment (2D collisions), so you’ll have to roll your own.

The theory is simply:

  • Create a few Prefabs that represent your particles (give them the required physics, sprite, life-cycle management, etc)
  • Create a new script on your block that, when called, generates a bunch of your Prefab objects, sets their velocities at random directions and strengths and then deletes the block.

This will get you started.

You’ll want to pool those particle prefab instances eventually, rather than keep creating them and destroying them, but you can do that after testing the core idea.

So I went the way to create my own Sprite Script Emitter.
As I am still newbie in Unity and specially in Physics I’m placing the script here for review and for any improvements from other users.

How to use the script:

  1. Create an empty GameObject in the scene.
  2. Drag the script into the new GameObject.
  3. Add the sprite(s) that you wish to be used as particles in the “sprites” parameter of the script.
    And that is all.

1743014--110218--Untitled.png

To test it via the inspector at runtime press the “emit” check-box, or by code call emitParticles()
Play with the different settings such as Force and Sparse-Direction to alter the collapse/explosion effect.
The particles scale is adjusted via the Transform Scale of the GameObject

Facts about the script:

-This script is not a replacement for the ParticlesSystem, neither tries to emulate the same functionality.
-Particles are emitted all at the same time as this is what I needed for my scenario.
-Collision is handled at layer level.
-As I said I’m still a newbie in Unity, so probably there are better ways to do it and with better performance. The script works fine for what I need it for, but if anyone can see a way to improve it or add functionality then please submit your changes.

The script:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class SpriteParticlesEmitter : MonoBehaviour
{
    private class Particle
    {
        private GameObject _gameObject;
        private float _lifespan;
        private float _intialLifespan;
        private Vector3 _position = new Vector3();
        private Vector3 _scale = new Vector3();
        private Vector2 _force = new Vector2();

        public Particle(GameObject value)
        {
            this._gameObject = value;
            this._gameObject.SetActive(false);
        }

        public bool isActive()
        {
            return this._gameObject.activeSelf;
        }

        public void setActive(bool value)
        {
            this._gameObject.SetActive(value);
            this.setScale(this._scale);
        }

        public void setPosition(Vector3 value)
        {
            this._position.Set(value.x, value.y, value.z);
            this._gameObject.transform.position = this._position;
        }

        public float lifespan
        {
            get { return this._lifespan; }
            set
            {
                this._lifespan = value;
                this._intialLifespan = value;
            }
        }

        public void setScale(Vector3 value)
        {
            this._scale.Set(value.x, value.y, value.z);
            this._gameObject.transform.localScale = this._scale;
        }

        public void dissipate()
        {
            this._lifespan -= Time.deltaTime;

            if (this._lifespan < this._intialLifespan / 3.0f)
            {
                this._gameObject.transform.localScale = Vector3.Lerp(Vector3.zero,
                                                                    this._scale,
                                                                    this._lifespan / this._intialLifespan);
            }
        }

        public void setForce(float forceX, float forceY)
        {
            this._force.Set(forceX, forceY);
            this._gameObject.rigidbody2D.AddForce(this._force, ForceMode2D.Impulse);
        }

        public void setVelocity(Vector2 value)
        {
            this._gameObject.rigidbody2D.velocity = value;
        }

        public void setGravityScale(int value)
        {
            this._gameObject.rigidbody2D.gravityScale = value;
            this._gameObject.rigidbody2D.drag = Random.Range(0.0f, 3.0f);
        }

        public void canCollide(bool value)
        {
            this._gameObject.collider2D.enabled = value;
        }

        public void destroy()
        {
            Destroy(this._gameObject);
        }
    }

    public bool emit = false;
    public bool loop = false;
    public bool dissipate = false;

    [Range(0, 50)]
    public int particleAmount = 10;
    [Range(0, 10)]
    public float particleLifespan = 3f;
    [Range(0, 10)]
    public int force = 5;
    public Vector2 sparseDirection;
    [Range(1, 4)]
    public int gravityScale = 1;
    [Range(0, 1)]
    public float bounciness = 0;
    [Range(0, 1)]
    public float friction = 1.0f;

    public LayerMask collideWith = Physics2D.AllLayers;
    public Sprite[] sprites;
  
    private List<Particle> _particlesPool;
    private int _enabledPooledParticles;
    private int _lifeParticles = 0;
    private LayerMask _collidesWith;
    private Vector3 _spriteScale;
    private PhysicsMaterial2D _particlesMaterial;

    /// <summary>
    /// Awake this instance
    /// </summary>
    public void Awake()
    {
        if (this.sprites == null || this.sprites.Length == 0)
        {
            Debug.LogError("Failed. Script has no sprites.", this.gameObject);
            this.enabled = false;
            return;
        }

        this._spriteScale = new Vector3(-1.0f, -1.0f, -1.0f);
        this._particlesMaterial = new PhysicsMaterial2D(this.name + "_material");
        this._particlesMaterial.bounciness = this.bounciness;
        this._particlesMaterial.friction = this.friction;
      
        this.setupParticlesPool();
        this.adjustCollisionLayers();
    }

    /// <summary>
    /// Update this instance
    /// </summary>
    void Update()
    {
        if (this.particleAmount != this._enabledPooledParticles)
        {
            this.setupParticlesPool();
        }

        if (this._collidesWith != this.collideWith)
        {
            this.adjustCollisionLayers();
        }

        if ((this.emit == true) || (this.loop == true && this._lifeParticles <= 0))
        {
            this.emit = false;

            StopCoroutine("updateParticles");
            this._lifeParticles = this.particleAmount;
            StartCoroutine("updateParticles", true);
        }
      
        if (this._spriteScale != this.transform.localScale ||
            this._particlesMaterial.bounciness != this.bounciness ||
            this._particlesMaterial.friction != this.friction)
        {
            for (int index = 0; index < this.particleAmount; index++)
            {
                this._particlesMaterial.bounciness = this.bounciness;
                this._particlesMaterial.friction = this.friction;
                this._spriteScale = this.transform.localScale;
                this._particlesPool[index].setScale(this._spriteScale);
            }
        }
    }

    /// <summary>
    /// Executes a particles emission
    /// </summary>
    public void emitParticles()
    {
        this.emit = false;
        StopCoroutine("updateParticles");
        this._lifeParticles = this.particleAmount;
        StartCoroutine("updateParticles", true);
    }

    /// <summary>
    /// Update all the active pooled particles
    /// </summary>
    /// <param name="emitParticles">True to emit the particles</param>
    private void updateParticles(bool emitParticles)
    {
        StopCoroutine("watchParticlesLifespan");

        for (int index = 0; index < this.particleAmount; index++)
        {
            Particle particle = this._particlesPool[index];

            particle.setActive(true);
            particle.setScale(this._spriteScale);

            particle.lifespan = Random.Range(this.particleLifespan, this.particleLifespan + 2);
            particle.setPosition(this.transform.position);
            particle.setGravityScale(Random.Range(this.gravityScale, this.gravityScale + 1));
            particle.setVelocity(Vector2.zero);
          
            float forceToApply = Random.Range(this.force, this.force + 1.0f);
            float forceX;
            float forceY;

            if (this.sparseDirection.x == 0)
            {
                forceX = Random.Range(-forceToApply, forceToApply);
            }
            else
            {
                forceX = Random.Range(this.sparseDirection.x, this.sparseDirection.x * forceToApply);
            }

            if (this.sparseDirection.y == 0)
            {
                forceY = Random.Range(-forceToApply, forceToApply);
            }
            else
            {
                forceY = Random.Range(this.sparseDirection.y, this.sparseDirection.y * forceToApply);
            }

            particle.setForce(forceX, forceY);
            particle.canCollide(this.collideWith.value != 0);
            particle.lifespan = Random.Range(this.particleLifespan, this.particleLifespan + 1.0f);
        }

        if (emitParticles == true && this._lifeParticles > 0)
        {
            StartCoroutine("watchParticlesLifespan");
        }
    }

    /// <summary>
    /// Create a new single Particle
    /// </summary>
    /// <param name="name">Particle name</param>
    /// <returns>New created particle</returns>
    private Particle createNewParticle(string name)
    {
        GameObject item = new GameObject(name);
      
        item.transform.parent = this.transform;
        item.layer = this.gameObject.layer;

        item.AddComponent<SpriteRenderer>();
        SpriteRenderer spriteRenderer = (SpriteRenderer)item.renderer;
        int randomSpriteIndex = Random.Range(0, this.sprites.Length);
        spriteRenderer.sprite = this.sprites[randomSpriteIndex];

        item.AddComponent<Rigidbody2D>();

        // Using EdgeCollider2D, because it doesn't
        // collide with others of the same type

        item.AddComponent<EdgeCollider2D>();
        ((EdgeCollider2D)item.collider2D).sharedMaterial = this._particlesMaterial;

        // Give some shape/volume to the edge collider

        float sizeFactorX = spriteRenderer.sprite.bounds.extents.x;
        float sizeFactorY = spriteRenderer.sprite.bounds.extents.y;
      
        ((EdgeCollider2D)item.collider2D).points = new Vector2[]
                                                    {
                                                        new Vector2(0.0f, -sizeFactorY),
                                                        new Vector2(0.0f, sizeFactorY),
                                                        new Vector2(sizeFactorX, 0.0f),
                                                        new Vector2(-sizeFactorX, 0.0f)
                                                    };

        Particle particle = new Particle(item);
        return particle;
    }

    /// <summary>
    /// Setups the particles pool. Note that the pool does not shrink
    /// </summary>
    private void setupParticlesPool()
    {
        if (this._particlesPool == null)
        {
            this._particlesPool = new List<Particle>();
        }

        StopCoroutine("updateParticles");
        StopCoroutine("watchParticlesLifespan");
      
        int totalParticles = Mathf.Max(this.particleAmount, this._particlesPool.Count);

        for (int index = 0; index < totalParticles; index++)
        {
            if (index == this._particlesPool.Count)
            {
                Particle particle = this.createNewParticle(this.name + "_particle_" + index);
                this._particlesPool.Add(particle);
            }

            this._particlesPool[index].setActive(false);
        }

        this._enabledPooledParticles = this.particleAmount;
        this._lifeParticles = 0;
    }

    /// <summary>
    /// Watch each particle lifespan
    /// </summary>
    /// <returns>IEnumerator as this method is to be called as a coroutine</returns>
    IEnumerator watchParticlesLifespan()
    {
        while (this._lifeParticles > 0)
        {
            for (int index = 0; index < this._enabledPooledParticles; index++)
            {
                Particle particle = this._particlesPool[index];

                if (particle.isActive() == true)
                {
                    if (dissipate == true)
                    {
                        particle.dissipate();
                    }
                    else
                    {
                        particle.lifespan -= Time.deltaTime;
                    }

                    if (particle.lifespan <= 0)
                    {
                        particle.lifespan = 0;
                        particle.setActive(false);
                        this._lifeParticles--;
                    }
                }
            }

            yield return 0; // Wait one frame
        }
    }

    /// <summary>
    /// Adjust the collision layers against which the particles can collide
    /// </summary>
    private void adjustCollisionLayers()
    {
        this._collidesWith = this.collideWith;
        int objectLayerId = this.gameObject.layer;

        for (int layerId = 0; layerId < 32; layerId++)
        {
            bool ignoreLayer = !(this.collideWith == (this.collideWith | (1 << layerId)));
            Physics2D.IgnoreLayerCollision(objectLayerId, layerId, ignoreLayer);
        }
    }

    /// <summary>
    /// Destroy the emitter
    /// </summary>
    public void destroy()
    {
        this.loop = false;
        this.emit = false;

        StopCoroutine("updateParticles");
        StopCoroutine("watchParticlesLifespan");

        if (this._particlesPool != null && this._particlesPool.Count > 0)
        {
            foreach (Particle particle in this._particlesPool)
            {
                particle.destroy();
            }
        }

        Destroy(this.gameObject);
    }
}
2 Likes

very nice work