Search Unity

Instantiating bomb / bullet objects

Discussion in '2D' started by MoonJellyGames, Jul 2, 2015.

  1. MoonJellyGames

    MoonJellyGames

    Joined:
    Oct 2, 2014
    Posts:
    331
    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.


    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. // collision code is based on tutorial by Sebastian Lague on YouTube
    5.  
    6. public class PlayerController : MonoBehaviour
    7. {
    8.    
    9.     float speed = 5;
    10.     float playerAcceleration = 30;
    11.     public LayerMask collisionMask;
    12.     public bool grounded;
    13.     public float jumpRate;
    14.     public bool canThrow = false;
    15.     public bool canCook = true;
    16.    
    17.     private int coolDown;                // time delay after throwing a bomb before you can throw another.  Default is equal to bomb.timeToDetonate
    18.     private bool stuck = false;            // true if stuck to a wall (clinging)
    19.     private int throwPower = 10;        // how hard can this player throw a bomb
    20.     private bool hittingWall = false;
    21.     public bool jumping;
    22.     private bool hitCeiling = false;
    23.     public float jumpLimit = 15;
    24.     public float jumpCounter = 0;
    25.     public float gravity = 40;
    26.     private float currentSpeed;
    27.     private float targetSpeed;
    28.     private float skin = 0.005f;
    29.     private Vector2 amountToMove;
    30.     private BoxCollider2D box;
    31.     private Vector2 size;
    32.     private Vector2 centre;
    33.     private Ray2D ray;
    34.     private RaycastHit2D hit;
    35.     private BombScript bomb;
    36.     private BombScript bombClone;
    37.    
    38.    
    39.     // Use this for initialization
    40.     void Start ()
    41.     {
    42.         box = GetComponent<BoxCollider2D>();
    43.         size = box.size;
    44.         centre = box.center;
    45.        
    46.         bomb = GetComponentInChildren<BombScript>();
    47.         bombClone = bomb;
    48.        
    49.         coolDown = bomb.getTimeToDetonate();
    50.        
    51.         jumpRate = 8;
    52.         jumping = false;
    53.     }
    54.    
    55.     void Update ()
    56.     {
    57.         if (hittingWall)
    58.         {
    59.             targetSpeed = 0;
    60.             currentSpeed = 0;
    61.         }
    62.        
    63.         targetSpeed = Input.GetAxis("Horizontal") * speed;
    64.         currentSpeed = IncrementTowards(currentSpeed, targetSpeed, playerAcceleration);
    65.        
    66.         if (currentSpeed > 0 && transform.localScale.x < 0)
    67.             Flip();
    68.         else if (currentSpeed < 0 && transform.localScale.x > 0)
    69.             Flip();
    70.        
    71.         if (grounded)
    72.         {
    73.             jumping = false;
    74.             jumpCounter = 0;
    75.            
    76.             if (Input.GetButtonDown("Jump"))
    77.                 jumping = true;
    78.         }
    79.        
    80.         // don't move up if you hit a ceiling
    81.         if (hitCeiling)
    82.         {
    83.             amountToMove.y = 0;
    84.         }
    85.        
    86.         if (jumping)
    87.         {
    88.             // jump
    89.             if (Input.GetButton("Jump") && (jumpCounter < jumpLimit) && !hitCeiling)
    90.             {
    91.                 //jumping = true;
    92.                 amountToMove.y = jumpRate;
    93.                 jumpCounter++;
    94.             }
    95.             else
    96.             {
    97.                 jumping = false;
    98.             }
    99.         }
    100.        
    101.         amountToMove.x = currentSpeed;
    102.        
    103.         // only apply gravity if not already grounded
    104.         if (!grounded)
    105.             amountToMove.y -= gravity * Time.deltaTime;
    106.        
    107.         Move (amountToMove * Time.deltaTime);
    108.        
    109.         // Cook a bomb: Hold down the Throw button
    110.         if (Input.GetButtonDown ("Throw") && canCook)
    111.         {
    112.             bombClone = (BombScript) Instantiate (bomb, transform.position, transform.rotation);
    113.             bombClone.transform.parent = this.transform;
    114.  
    115.             bombClone.Cook();
    116.             canCook = false;
    117.             canThrow = true;
    118.         }
    119.  
    120.         // Throw a bomb:  Release the Throw button
    121.         if (Input.GetButtonUp ("Throw") && canThrow)
    122.         {
    123.             // if you're holding the down key, drop the bomb
    124.             if (Input.GetAxis("Vertical") < 0)
    125.                 bombClone.Throw(new Vector2 (transform.localScale.x, 0.5f), 0);
    126.             else
    127.                 bombClone.Throw(new Vector2 (transform.localScale.x, 0.5f), throwPower);
    128.            
    129.             canThrow = false;
    130.             canCook = true; // temp: canCook should be made true after the coolDown timer expires, but that isn't worked in yet
    131.         }
    132.     }
    133.    
    134.     // increment towards target by speed
    135.     private float IncrementTowards(float n, float target, float acc)
    136.     {
    137.         if (n == target)
    138.             return n;
    139.         else
    140.         {
    141.             float dir = Mathf.Sign(target - n);
    142.             n += acc * Time.deltaTime * dir;
    143.            
    144.             return (dir == Mathf.Sign(target - n)? n: target);
    145.         }
    146.     }
    147.    
    148.     private void Move(Vector2 moveAmount)
    149.     {
    150.         float deltaY = moveAmount.y;
    151.         float deltaX = moveAmount.x;
    152.         Vector2 position = transform.position;
    153.        
    154.         // above and below collisions
    155.         grounded = false;
    156.         hitCeiling = false;
    157.        
    158.         for (int i = 0; i < 3; i++)
    159.         {
    160.             float dir = Mathf.Sign (deltaY);
    161.             float x = (position.x + centre.x - (size.x / 2)) + (size.x / 2) * i;
    162.             float y = position.y + centre.y + size.y / 2 * dir;
    163.            
    164.             ray = new Ray2D(new Vector2(x,y + (skin * Mathf.Sign (dir))), new Vector2(0, dir));
    165.             Debug.DrawRay(ray.origin, ray.direction);
    166.            
    167.             hit = Physics2D.Raycast(ray.origin, new Vector2(0, dir), Mathf.Abs(deltaY) + skin, collisionMask);
    168.            
    169.             if (hit)
    170.             {
    171.                 float dst = Vector2.Distance(ray.origin, hit.point);
    172.                
    173.                 // Stop player's downward movement after coming within skin width of a collider
    174.                 if (dst > skin)
    175.                 {
    176.                     deltaY = dst * dir + skin * dir * 0.5f;
    177.                 }
    178.                 else
    179.                 {
    180.                     deltaY = 0;
    181.                 }
    182.                
    183.                 if (dir > 0)
    184.                     hitCeiling = true;
    185.                 else
    186.                     grounded = true;
    187.                 break;
    188.                
    189.             }
    190.         }
    191.        
    192.         // left and right collisions
    193.        
    194.         hittingWall = false;
    195.        
    196.         for (int i = 0; i < 3; i++)
    197.         {
    198.             float dir = Mathf.Sign (deltaX);
    199.             float x = position.x + centre.x + size.x / 2 * dir;
    200.             float y = position.y + centre.y - size.y / 2 + size.y / 2 * i;
    201.            
    202.             ray = new Ray2D(new Vector2(x,y), new Vector2(dir, 0));
    203.             Debug.DrawRay(ray.origin, ray.direction);
    204.            
    205.             hit = Physics2D.Raycast(ray.origin, new Vector2(dir, 0), Mathf.Abs(deltaX) + skin, collisionMask);
    206.            
    207.             if (hit)
    208.             {
    209.                 float dst = Vector2.Distance(ray.origin, hit.point);
    210.                
    211.                 // Stop player's downward movement after coming within skin width of a collider
    212.                 if (dst > skin)
    213.                 {
    214.                     deltaX = dst * dir - skin * dir;
    215.                 }
    216.                 else
    217.                 {
    218.                     deltaX = 0;
    219.                 }
    220.                 hittingWall = true;
    221.                 break;
    222.                
    223.             }
    224.         }
    225.        
    226.         Vector2 finalTransform = new Vector2(deltaX, deltaY);
    227.        
    228.         transform.Translate(finalTransform);
    229.     } // END OF- MOVE
    230.    
    231.     private void Flip()
    232.     {
    233.         transform.localScale = new Vector3(-transform.localScale.x, transform.localScale.y, transform.localScale.z);
    234.     }
    235.    
    236. }
    237.  
    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<SpriteRenderer>().

    Does anybody know what I'm doing wrong here? Thanks!
     
  2. Outwizard

    Outwizard

    Joined:
    Jun 25, 2015
    Posts:
    22
    Can you show the Bombscript.cs please?
     
  3. MoonJellyGames

    MoonJellyGames

    Joined:
    Oct 2, 2014
    Posts:
    331
    Sure. Here it is:


    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4.  
    5. public class BombScript : AbstractExplosive
    6. {
    7.     BlastScript blast;    // this is not really necessary anymore, but the blast child object just has a large CircleCollider2D which is where
    8.                         // the blast radius comes from, as well as the blast radius sprite (also place-holder)
    9.     CircleCollider2D circleCollider;    // a quick reference to this object's CircleCollider2D
    10.     Rigidbody2D body;                    // a quick reference to this object's Rigidbody2D
    11.     SpriteRenderer sprite;                // a quick reference to this object's SpriteRenderer
    12.     List<Collider2D> hits;                // a list of each object caught in the explosion
    13.     float blastRadius;                    //
    14.     private bool fuseStarted = false;
    15.     private int timeToDetonate = 3;        // time to detonate (in seconds);
    16.  
    17.     public float fuseTimer = 0;
    18.     public bool exploded = false;        // just used for testing.  get rid of this later.
    19.     public int blastPower = 25;
    20.  
    21.     // Use this for initialization
    22.     void Start ()
    23.     {
    24.         blast = GetComponentInChildren<BlastScript>();
    25.         blastRadius = blast.GetComponent<CircleCollider2D>().radius;
    26.         circleCollider = GetComponent<CircleCollider2D>();
    27.         body = GetComponent<Rigidbody2D>();
    28.         sprite = GetComponent<SpriteRenderer>();
    29.  
    30.         hits = new List<Collider2D>();
    31.  
    32.         sprite.enabled = false;
    33.     }
    34.    
    35.     // Update is called once per frame
    36.     void Update ()
    37.     {
    38.         // detonate bomb and display hits size. This function is just for testing.
    39.         if (Input.GetKeyDown(KeyCode.B))
    40.         {
    41.             hits.Clear();
    42.             Explode();
    43.             Debug.Log("num hits: " + hits.Count);
    44.         }
    45.  
    46.          // draw lines to hits
    47.         foreach (Collider2D col in hits)
    48.         {
    49.             Debug.DrawRay(this.transform.position, (col.transform.position - this.transform.position),
    50.                           Color.blue);
    51.         }
    52.  
    53.         if (fuseStarted)
    54.         {
    55.             if (fuseTimer > 0)
    56.             {
    57.                 fuseTimer--;
    58.                 // some visual representation of the fuse
    59.                 sprite.color = Color.Lerp(sprite.color, Color.red, 0.01f);
    60.             }
    61.             else
    62.             {
    63.                 fuseTimer = 0;
    64.                 fuseStarted = false;
    65.                 Explode ();
    66.             }
    67.         }
    68.     }
    69.  
    70.     public override void TakeExplosionDamage(Vector2 dir, int dmg)
    71.     {
    72.         Explode();
    73.     }
    74.  
    75.     public void Cook()
    76.     {
    77.         Debug.Log("calling cook");
    78.  
    79.         sprite.enabled = true;        // this is the line that the above-mentioned exception points to.
    80.  
    81.         fuseTimer = timeToDetonate * 60;        // 5 * 60fps = 5 seconds
    82.         fuseStarted = true;
    83.     }
    84.  
    85.     public void Throw(Vector2 playerPos, int pow)
    86.     {
    87.         // calc throw direction and move bomb with physics
    88.         Vector2 dir = playerPos - (Vector2) this.transform.position;
    89.         circleCollider.enabled = true;
    90.         body.isKinematic = false;
    91.         transform.parent = null;
    92.  
    93.         rigidbody2D.velocity = playerPos * pow;
    94.     }
    95.  
    96.     void Explode()
    97.     {
    98.         rigidbody2D.isKinematic = true;
    99.         circleCollider.enabled = false;        //not sure why this is here
    100.  
    101.         DetermineObjectsInRadius();
    102.         // DetermineHits();                    // This part of the explosion hits calculation has been omitted.
    103.                                             // It's much too slow, and probably not ideal from a design point either.
    104.         SendExplosionMessages();
    105.  
    106.         circleCollider.enabled = true;
    107.         rigidbody2D.isKinematic = false;
    108.  
    109.         // play explosion animation (I think the Unity tutorial did this by instantiating a different object)
    110.  
    111.         //test
    112.         exploded = true;
    113.  
    114.         // Destroy this object
    115.         Destroy(this.gameObject);
    116.     }
    117.  
    118.     // Collects all objects within the blast radius and adds them to the hits list
    119.     void DetermineObjectsInRadius()
    120.     {
    121.         foreach (Collider2D col in Physics2D.OverlapCircleAll(transform.position, blastRadius))
    122.         {
    123.             hits.Add(col);
    124.         }
    125.  
    126.         //Debug.Log("num obj in radius " + hits.Count);
    127.     }
    128.  
    129.     // Determines which objects will actually be hit by the explosion. Parses out non-hits
    130.     void DetermineHits()
    131.     {
    132.         Vector2 start = transform.position;
    133.  
    134.         for (int i = 0; i < hits.Count; i++)
    135.         {
    136.             Vector2 end = hits[i].transform.position;
    137.             RaycastHit2D hitObject = Physics2D.Raycast(start, (end - start));
    138.  
    139. //            Debug.Log("Obj in radius: " + hits[i].GetComponent<BreakableScript>().objID + ", raycastHit: "
    140. //                      + hitObject.collider.gameObject.GetComponent<BreakableScript>().objID);
    141.  
    142.             // first do a raycast check to the center of the target "hits" object
    143.             if (hits[i].gameObject.GetInstanceID() != hitObject.collider.gameObject.GetInstanceID())
    144.             {
    145.                 // if the raycast to the center doesn't hit the target, try the corners.  If still nothing is found, remove from "hits"
    146.                 if (!RaycastEdges(hits[i]))
    147.                 {
    148.                     hits.RemoveAt(i);    // remove the object that is obstructed
    149.                     i--;                // reel the iterator back by one to account for the removed object
    150.                 }
    151.  
    152.                 // use these two lines for center hit check only.  (This method looks kinda wonky)
    153. //                hits.RemoveAt(i);
    154. //                i--;
    155.             }
    156.         }
    157.     }
    158.  
    159.     // raycast to epoints around the edge of a collider to see if it matches the collider in the blast area
    160.     bool RaycastEdges(Collider2D col)
    161.     {
    162.         List<Vector2> edges = new List<Vector2>();
    163.         float posX = col.transform.position.x;
    164.         float posY = col.transform.position.y;
    165.         float halfWidth;
    166.         float halfHeight;
    167.  
    168.         Vector2 start = transform.position;
    169.         Vector2 end;    // each corner will be set as an end to set the direction of the raycast
    170.         RaycastHit2D hitObject;
    171.  
    172.         if (col is CircleCollider2D)
    173.         {
    174.             halfWidth = halfHeight = (col as CircleCollider2D).radius;
    175.         }
    176.         else
    177.         {
    178.             halfWidth = (col as BoxCollider2D).size.x / 2;
    179.             halfHeight = (col as BoxCollider2D).size.y / 2;
    180.         }
    181.  
    182.         // each corner of the collider
    183.         Vector2 topLeft = new Vector2(posX - halfWidth, posY + halfHeight);
    184.         Vector2 topRight = new Vector2(posX + halfWidth, posY + halfHeight);
    185.         Vector2 bottomLeft = new Vector2(posX - halfWidth, posY - halfHeight);
    186.         Vector2 bottomRight = new Vector2(posX + halfWidth, posY - halfHeight);
    187.  
    188.         // mid-points around the perimeter
    189.         Vector2 topCentre = new Vector2(posX, posY + halfHeight);
    190.         Vector2 bottomCentre = new Vector2(posX, posY - halfHeight);
    191.         Vector2 leftCentre = new Vector2(posX - halfWidth, posY);
    192.         Vector2 rightCentre = new Vector2(posX + halfWidth, posY);
    193.  
    194.         edges.Add(topLeft);
    195.         edges.Add(topRight);
    196.         edges.Add(bottomLeft);
    197.         edges.Add (bottomRight);
    198.         edges.Add (topCentre);
    199.         edges.Add (bottomCentre);
    200.         edges.Add (leftCentre);
    201.         edges.Add (rightCentre);
    202.  
    203.         foreach (Vector2 edge in edges)
    204.         {
    205.             end = edge;
    206.             hitObject = Physics2D.Raycast(start, (end - start));
    207.  
    208.             Debug.Log("hitObject: " + hitObject.collider);
    209.  
    210.             if (hitObject.collider != null)
    211.             {
    212.                 if (col.gameObject == hitObject.collider.gameObject)
    213.                     return true;
    214.             }
    215.         }
    216.  
    217.         return false;
    218.     }
    219.  
    220.     void SendExplosionMessages()
    221.     {
    222.         foreach (Collider2D col in hits)
    223.         {
    224.             if (col.gameObject.GetComponent<AbstractExplosive>() != null)
    225.             {
    226.                 // the damage sent to the hit object is lessened by the distance between the object and the bomb.  This needs work still.
    227.                 int damage = (int) (blastPower * ((1 / Vector2.Distance(this.transform.position, col.transform.position))));
    228.                 Vector2 force = (col.gameObject.transform.position - transform.position);
    229.  
    230.                 // 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)
    231.                 col.gameObject.GetComponent<AbstractExplosive>().TakeExplosionDamage(force, damage);
    232.             }
    233.         }
    234.  
    235.         // hits.Clear();    // Empty the list of hits.  Comment this line out to see raycasts.  May cause missing object exceptions
    236.     }
    237.  
    238.     public int getTimeToDetonate()
    239.     {
    240.         return timeToDetonate;
    241.     }
    242.  
    243.     // just for testing
    244.     public bool GetSprite()
    245.     {
    246.         return sprite;
    247.     }
    248.  
    249.     // just for testing
    250.     public bool GetSpriteRenderer()
    251.     {
    252.         return GetComponent<SpriteRenderer>();
    253.     }
    254. }
    255.  
     
  4. Outwizard

    Outwizard

    Joined:
    Jun 25, 2015
    Posts:
    22
    Try changing:

    Code (CSharp):
    1.  void Start ()
    2.     {
    3.         blast = GetComponentInChildren<BlastScript>();
    4.         ...
    5.         circleCollider = GetComponent<CircleCollider2D>();
    6.         body = GetComponent<Rigidbody2D>();
    7.         sprite = GetComponent<SpriteRenderer>();
    8.         ...
    9.     }
    To:

    Code (CSharp):
    1.  void Start ()
    2.     {
    3.         blast = gameObject.GetComponentInChildren<BlastScript>();
    4.         ...
    5.         circleCollider = gameObject.GetComponent<CircleCollider2D>();
    6.         body = gameObject.GetComponent<Rigidbody2D>();
    7.         sprite = gameObject.GetComponent<SpriteRenderer>();
    8.         ...
    9.     }

    Bear Boom
    iOS: https://itunes.apple.com/us/app/id985992564?mt=8
    Android: https://play.google.com/store/apps/details?id=com.Airbear.Airbear
     
    Last edited: Jul 3, 2015
  5. MoonJellyGames

    MoonJellyGames

    Joined:
    Oct 2, 2014
    Posts:
    331
    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:

    Code (CSharp):
    1. bombClone = (GameObject) Instantiate (bomb, transform.position, transform.rotation);
    2. 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:

    Code (CSharp):
    1.     // Awake is always called before Start()
    2.     void Awake()
    3.     {
    4.         // this has to happen in Awake() aparently.
    5.         sprite = GetComponent<SpriteRenderer>();
    6.         sprite.enabled = false;
    7.     }
    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. :)