Instantiating bomb / bullet objects

I know this is a commonly-asked question, but nothing I’ve read has clued me in to why I can’t get this thing working.

What I’m doing is simple enough: I have a player (I will have at least two) who can throw bombs. By default, each player can only throw one bomb at a time.

I will eventually be working in 360 degree aiming with a Dualshock3 controller, but for now, aiming works like this: Hold down the “Throw” key to “cook” a bomb (start it’s detonation timer), and release the “Throw” key to throw the bomb in whichever direction you’re facing. You can also hold down the down arrow key to drop the bomb at your feet.

While cooking a bomb, you can move around with it. To allow for this, I’ve made the player object have a bomb object as its child in the hierarchy. By default, the bomb’s sprite and collider are disabled, and its rigidbody is kinematic. Once you hold down “Throw”, the sprite becomes enabled, and when you throw, the collider and rigidbody switch states as well. There isn’t any collision between the player and the bomb. In fact, the player’s collider is a trigger-- all player collisions are handled with raycasting.

I just want to create a new bomb object (and have it set as a child of the player) when the “Throw” key is pressed if the bomb object isn’t destroyed. As you’d expect, that’s what happens in the last line of the bomb’s Explode() function.

This is the PlayerController script. Sorry it’s messy-- I have a lot of work to do on it still.

using UnityEngine;
using System.Collections;

// collision code is based on tutorial by Sebastian Lague on YouTube

public class PlayerController : MonoBehaviour
{
   
    float speed = 5;
    float playerAcceleration = 30;
    public LayerMask collisionMask;
    public bool grounded;
    public float jumpRate;
    public bool canThrow = false;
    public bool canCook = true;
   
    private int coolDown;                // time delay after throwing a bomb before you can throw another.  Default is equal to bomb.timeToDetonate
    private bool stuck = false;            // true if stuck to a wall (clinging)
    private int throwPower = 10;        // how hard can this player throw a bomb
    private bool hittingWall = false;
    public bool jumping;
    private bool hitCeiling = false;
    public float jumpLimit = 15;
    public float jumpCounter = 0;
    public float gravity = 40;
    private float currentSpeed;
    private float targetSpeed;
    private float skin = 0.005f;
    private Vector2 amountToMove;
    private BoxCollider2D box;
    private Vector2 size;
    private Vector2 centre;
    private Ray2D ray;
    private RaycastHit2D hit;
    private BombScript bomb;
    private BombScript bombClone;
   
   
    // Use this for initialization
    void Start ()
    {
        box = GetComponent<BoxCollider2D>();
        size = box.size;
        centre = box.center;
       
        bomb = GetComponentInChildren<BombScript>();
        bombClone = bomb;
       
        coolDown = bomb.getTimeToDetonate();
       
        jumpRate = 8;
        jumping = false;
    }
   
    void Update ()
    {
        if (hittingWall)
        {
            targetSpeed = 0;
            currentSpeed = 0;
        }
       
        targetSpeed = Input.GetAxis("Horizontal") * speed;
        currentSpeed = IncrementTowards(currentSpeed, targetSpeed, playerAcceleration);
       
        if (currentSpeed > 0 && transform.localScale.x < 0)
            Flip();
        else if (currentSpeed < 0 && transform.localScale.x > 0)
            Flip();
       
        if (grounded)
        {
            jumping = false;
            jumpCounter = 0;
           
            if (Input.GetButtonDown("Jump"))
                jumping = true;
        }
       
        // don't move up if you hit a ceiling
        if (hitCeiling)
        {
            amountToMove.y = 0;
        }
       
        if (jumping)
        {
            // jump
            if (Input.GetButton("Jump") && (jumpCounter < jumpLimit) && !hitCeiling)
            {
                //jumping = true;
                amountToMove.y = jumpRate;
                jumpCounter++;
            }
            else
            {
                jumping = false;
            }
        }
       
        amountToMove.x = currentSpeed;
       
        // only apply gravity if not already grounded
        if (!grounded)
            amountToMove.y -= gravity * Time.deltaTime;
       
        Move (amountToMove * Time.deltaTime);
       
        // Cook a bomb: Hold down the Throw button
        if (Input.GetButtonDown ("Throw") && canCook)
        {
            bombClone = (BombScript) Instantiate (bomb, transform.position, transform.rotation);
            bombClone.transform.parent = this.transform;

            bombClone.Cook();
            canCook = false;
            canThrow = true;
        }

        // Throw a bomb:  Release the Throw button
        if (Input.GetButtonUp ("Throw") && canThrow)
        {
            // if you're holding the down key, drop the bomb
            if (Input.GetAxis("Vertical") < 0)
                bombClone.Throw(new Vector2 (transform.localScale.x, 0.5f), 0);
            else
                bombClone.Throw(new Vector2 (transform.localScale.x, 0.5f), throwPower);
           
            canThrow = false;
            canCook = true; // temp: canCook should be made true after the coolDown timer expires, but that isn't worked in yet
        }
    }
   
    // increment towards target by speed
    private float IncrementTowards(float n, float target, float acc)
    {
        if (n == target)
            return n;
        else
        {
            float dir = Mathf.Sign(target - n);
            n += acc * Time.deltaTime * dir;
           
            return (dir == Mathf.Sign(target - n)? n: target);
        }
    }
   
    private void Move(Vector2 moveAmount)
    {
        float deltaY = moveAmount.y;
        float deltaX = moveAmount.x;
        Vector2 position = transform.position;
       
        // above and below collisions
        grounded = false;
        hitCeiling = false;
       
        for (int i = 0; i < 3; i++)
        {
            float dir = Mathf.Sign (deltaY);
            float x = (position.x + centre.x - (size.x / 2)) + (size.x / 2) * i;
            float y = position.y + centre.y + size.y / 2 * dir;
           
            ray = new Ray2D(new Vector2(x,y + (skin * Mathf.Sign (dir))), new Vector2(0, dir));
            Debug.DrawRay(ray.origin, ray.direction);
           
            hit = Physics2D.Raycast(ray.origin, new Vector2(0, dir), Mathf.Abs(deltaY) + skin, collisionMask);
           
            if (hit)
            {
                float dst = Vector2.Distance(ray.origin, hit.point);
               
                // Stop player's downward movement after coming within skin width of a collider
                if (dst > skin)
                {
                    deltaY = dst * dir + skin * dir * 0.5f;
                }
                else
                {
                    deltaY = 0;
                }
               
                if (dir > 0)
                    hitCeiling = true;
                else
                    grounded = true;
                break;
               
            }
        }
       
        // left and right collisions
       
        hittingWall = false;
       
        for (int i = 0; i < 3; i++)
        {
            float dir = Mathf.Sign (deltaX);
            float x = position.x + centre.x + size.x / 2 * dir;
            float y = position.y + centre.y - size.y / 2 + size.y / 2 * i;
           
            ray = new Ray2D(new Vector2(x,y), new Vector2(dir, 0));
            Debug.DrawRay(ray.origin, ray.direction);
           
            hit = Physics2D.Raycast(ray.origin, new Vector2(dir, 0), Mathf.Abs(deltaX) + skin, collisionMask);
           
            if (hit)
            {
                float dst = Vector2.Distance(ray.origin, hit.point);
               
                // Stop player's downward movement after coming within skin width of a collider
                if (dst > skin)
                {
                    deltaX = dst * dir - skin * dir;
                }
                else
                {
                    deltaX = 0;
                }
                hittingWall = true;
                break;
               
            }
        }
       
        Vector2 finalTransform = new Vector2(deltaX, deltaY);
       
        transform.Translate(finalTransform);
    } // END OF- MOVE
   
    private void Flip()
    {
        transform.localScale = new Vector3(-transform.localScale.x, transform.localScale.y, transform.localScale.z);
    }
   
}

When I test this, the hierarchy shows new bomb(Clone) objects, but they aren’t visible and they don’t seem to have their colliders or rigidbodies enabled. It seems to be creating clones of the destroyed object, but as you can see, it’s the clone that I’m destroying (by calling Cook() and Throw()). Also, I get an exception:

NullReferenceException: Object reference not set to an instance of an object
BombScript.Cook () (at Assets/Scripts/BombScript.cs:81)
PlayerController.Update () (at Assets/Scripts/PlayerController.cs:115)

The line this is pointing to looks like this:

sprite.enabled = true;

In the BombScript, sprite is a SpriteRenderer object that, in the Start() function is set to GetComponent().

Does anybody know what I’m doing wrong here? Thanks!

Can you show the Bombscript.cs please?

Sure. Here it is:

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

public class BombScript : AbstractExplosive
{
    BlastScript blast;    // this is not really necessary anymore, but the blast child object just has a large CircleCollider2D which is where
                        // the blast radius comes from, as well as the blast radius sprite (also place-holder)
    CircleCollider2D circleCollider;    // a quick reference to this object's CircleCollider2D
    Rigidbody2D body;                    // a quick reference to this object's Rigidbody2D
    SpriteRenderer sprite;                // a quick reference to this object's SpriteRenderer
    List<Collider2D> hits;                // a list of each object caught in the explosion
    float blastRadius;                    //
    private bool fuseStarted = false;
    private int timeToDetonate = 3;        // time to detonate (in seconds);

    public float fuseTimer = 0;
    public bool exploded = false;        // just used for testing.  get rid of this later.
    public int blastPower = 25;

    // Use this for initialization
    void Start ()
    {
        blast = GetComponentInChildren<BlastScript>();
        blastRadius = blast.GetComponent<CircleCollider2D>().radius;
        circleCollider = GetComponent<CircleCollider2D>();
        body = GetComponent<Rigidbody2D>();
        sprite = GetComponent<SpriteRenderer>();

        hits = new List<Collider2D>();

        sprite.enabled = false;
    }
   
    // Update is called once per frame
    void Update ()
    {
        // detonate bomb and display hits size. This function is just for testing.
        if (Input.GetKeyDown(KeyCode.B))
        {
            hits.Clear();
            Explode();
            Debug.Log("num hits: " + hits.Count);
        }

         // draw lines to hits
        foreach (Collider2D col in hits)
        {
            Debug.DrawRay(this.transform.position, (col.transform.position - this.transform.position),
                          Color.blue);
        }

        if (fuseStarted)
        {
            if (fuseTimer > 0)
            {
                fuseTimer--;
                // some visual representation of the fuse
                sprite.color = Color.Lerp(sprite.color, Color.red, 0.01f);
            }
            else
            {
                fuseTimer = 0;
                fuseStarted = false;
                Explode ();
            }
        }
    }

    public override void TakeExplosionDamage(Vector2 dir, int dmg)
    {
        Explode();
    }

    public void Cook()
    {
        Debug.Log("calling cook");

        sprite.enabled = true;        // this is the line that the above-mentioned exception points to.

        fuseTimer = timeToDetonate * 60;        // 5 * 60fps = 5 seconds
        fuseStarted = true;
    }

    public void Throw(Vector2 playerPos, int pow)
    {
        // calc throw direction and move bomb with physics
        Vector2 dir = playerPos - (Vector2) this.transform.position;
        circleCollider.enabled = true;
        body.isKinematic = false;
        transform.parent = null;

        rigidbody2D.velocity = playerPos * pow;
    }

    void Explode()
    {
        rigidbody2D.isKinematic = true;
        circleCollider.enabled = false;        //not sure why this is here

        DetermineObjectsInRadius();
        // DetermineHits();                    // This part of the explosion hits calculation has been omitted.
                                            // It's much too slow, and probably not ideal from a design point either.
        SendExplosionMessages();

        circleCollider.enabled = true;
        rigidbody2D.isKinematic = false;

        // play explosion animation (I think the Unity tutorial did this by instantiating a different object)

        //test
        exploded = true;

        // Destroy this object
        Destroy(this.gameObject);
    }

    // Collects all objects within the blast radius and adds them to the hits list
    void DetermineObjectsInRadius()
    {
        foreach (Collider2D col in Physics2D.OverlapCircleAll(transform.position, blastRadius))
        {
            hits.Add(col);
        }

        //Debug.Log("num obj in radius " + hits.Count);
    }

    // Determines which objects will actually be hit by the explosion. Parses out non-hits
    void DetermineHits()
    {
        Vector2 start = transform.position;

        for (int i = 0; i < hits.Count; i++)
        {
            Vector2 end = hits[i].transform.position;
            RaycastHit2D hitObject = Physics2D.Raycast(start, (end - start));

//            Debug.Log("Obj in radius: " + hits[i].GetComponent<BreakableScript>().objID + ", raycastHit: "
//                      + hitObject.collider.gameObject.GetComponent<BreakableScript>().objID);

            // first do a raycast check to the center of the target "hits" object
            if (hits[i].gameObject.GetInstanceID() != hitObject.collider.gameObject.GetInstanceID())
            {
                // if the raycast to the center doesn't hit the target, try the corners.  If still nothing is found, remove from "hits"
                if (!RaycastEdges(hits[i]))
                {
                    hits.RemoveAt(i);    // remove the object that is obstructed
                    i--;                // reel the iterator back by one to account for the removed object
                }

                // use these two lines for center hit check only.  (This method looks kinda wonky)
//                hits.RemoveAt(i);
//                i--;
            }
        }
    }

    // raycast to epoints around the edge of a collider to see if it matches the collider in the blast area
    bool RaycastEdges(Collider2D col)
    {
        List<Vector2> edges = new List<Vector2>();
        float posX = col.transform.position.x;
        float posY = col.transform.position.y;
        float halfWidth;
        float halfHeight;

        Vector2 start = transform.position;
        Vector2 end;    // each corner will be set as an end to set the direction of the raycast
        RaycastHit2D hitObject;

        if (col is CircleCollider2D)
        {
            halfWidth = halfHeight = (col as CircleCollider2D).radius;
        }
        else
        {
            halfWidth = (col as BoxCollider2D).size.x / 2;
            halfHeight = (col as BoxCollider2D).size.y / 2;
        }

        // each corner of the collider
        Vector2 topLeft = new Vector2(posX - halfWidth, posY + halfHeight);
        Vector2 topRight = new Vector2(posX + halfWidth, posY + halfHeight);
        Vector2 bottomLeft = new Vector2(posX - halfWidth, posY - halfHeight);
        Vector2 bottomRight = new Vector2(posX + halfWidth, posY - halfHeight);

        // mid-points around the perimeter
        Vector2 topCentre = new Vector2(posX, posY + halfHeight);
        Vector2 bottomCentre = new Vector2(posX, posY - halfHeight);
        Vector2 leftCentre = new Vector2(posX - halfWidth, posY);
        Vector2 rightCentre = new Vector2(posX + halfWidth, posY);

        edges.Add(topLeft);
        edges.Add(topRight);
        edges.Add(bottomLeft);
        edges.Add (bottomRight);
        edges.Add (topCentre);
        edges.Add (bottomCentre);
        edges.Add (leftCentre);
        edges.Add (rightCentre);

        foreach (Vector2 edge in edges)
        {
            end = edge;
            hitObject = Physics2D.Raycast(start, (end - start));

            Debug.Log("hitObject: " + hitObject.collider);

            if (hitObject.collider != null)
            {
                if (col.gameObject == hitObject.collider.gameObject)
                    return true;
            }
        }

        return false;
    }

    void SendExplosionMessages()
    {
        foreach (Collider2D col in hits)
        {
            if (col.gameObject.GetComponent<AbstractExplosive>() != null)
            {
                // the damage sent to the hit object is lessened by the distance between the object and the bomb.  This needs work still.
                int damage = (int) (blastPower * ((1 / Vector2.Distance(this.transform.position, col.transform.position))));
                Vector2 force = (col.gameObject.transform.position - transform.position);

                // note: The first argument Vector2.up is just a place-holder as I don't need the direction of the explosion yet (temp force: col.gameObject.transform.position - transform.position)
                col.gameObject.GetComponent<AbstractExplosive>().TakeExplosionDamage(force, damage);
            }
        }

        // hits.Clear();    // Empty the list of hits.  Comment this line out to see raycasts.  May cause missing object exceptions
    }

    public int getTimeToDetonate()
    {
        return timeToDetonate;
    }

    // just for testing
    public bool GetSprite()
    {
        return sprite;
    }

    // just for testing
    public bool GetSpriteRenderer()
    {
        return GetComponent<SpriteRenderer>();
    }
}

Try changing:

 void Start ()
    {
        blast = GetComponentInChildren<BlastScript>();
        ...
        circleCollider = GetComponent<CircleCollider2D>();
        body = GetComponent<Rigidbody2D>();
        sprite = GetComponent<SpriteRenderer>();
        ...
    }

To:

 void Start ()
    {
        blast = gameObject.GetComponentInChildren<BlastScript>();
        ...
        circleCollider = gameObject.GetComponent<CircleCollider2D>();
        body = gameObject.GetComponent<Rigidbody2D>();
        sprite = gameObject.GetComponent<SpriteRenderer>();
        ...
    }

Bear Boom
iOS: https://itunes.apple.com/us/app/id985992564?mt=8
Android: https://play.google.com/store/apps/details?id=com.Airbear.Airbear

Thanks, Outwizard, but I just got it working about 30 minutes ago. I ended up doing basically the same thing that the example project does for the missiles: The Gun (or in my case, the Player) has a public GameObject that is set to a bomb in the Inspector instead of in script (though I imagine this could be done with Resources.Load(), but I don’t want to start putting stuff into a Resources folder when everything is already organized to my liking).

There is also a private GameObject class member, bombClone. When a bomb is needed, I do the following:

bombClone = (GameObject) Instantiate (bomb, transform.position, transform.rotation);
bombClone.transform.parent = this.transform;

and bombClone is used for everything. Aside from the public GameObject part, this is pretty similar to the stuff I was trying. One persistent problem was the inability to recognize that sprite (in BombScript) should have been initialized. As you can see, it is set in the Start() function. I tried removing the SpriteRenderer set-up from Start() and doing this instead:

    // Awake is always called before Start()
    void Awake()
    {
        // this has to happen in Awake() aparently.
        sprite = GetComponent<SpriteRenderer>();
        sprite.enabled = false;
    }

And that seemed to do it. It’s strange because certain otherwise unsuccessful attempts at making and throwing bombs worked for everything except for the SpriteRenderer. Invisible bombs would damage my terrain, and when pausing the game window, I could click on the spawned “bomb (Clone)” objects in the hierarchy to highlight them in the Scene View and confirm that they had a working collider and rigidbody.

It’s frustrating that this was so difficult when I don’t think it should have been, but I’ve got it now. Hopefully somebody else will find this useful.

I have another question about my bombs, but I’ll save it for another thread. Thanks for reading. :slight_smile: