ud3p
ud3p

Reputation: 31

Fast moving objects 2D game (Unity3d)

I find the official unity training https://www.youtube.com/watch?v=D5MqLcO6A8g and find bug.(look at the score)

I spent about 2 days to fix it and failed. I find the "DontGoThroughThings" script and try to rewrite to use in 2D. Failed again)

Please help me!

This is rewrite script:

public LayerMask layerMask; //make sure we aren't in this layer 
public float skinWidth; //probably doesn't need to be changed 

private float minimumExtent; 
private float partialExtent; 
private float sqrMinimumExtent; 
private Vector2 previousPosition; 
private Rigidbody2D myRigidbody; 


//initialize values 
void Awake() 
{ 
    myRigidbody = GetComponent<Rigidbody2D>(); 
    previousPosition = myRigidbody.position;
    minimumExtent = Mathf.Min(Mathf.Min(GetComponent<Collider2D>().bounds.extents.x, GetComponent<Collider2D>().bounds.extents.y)); 
    partialExtent = minimumExtent * (1.0f - skinWidth); 
    sqrMinimumExtent = minimumExtent * minimumExtent; 
} 

void FixedUpdate() 
{ 
    //have we moved more than our minimum extent? 
    Vector2 movementThisStep = myRigidbody.position - previousPosition; 
    float movementSqrMagnitude = movementThisStep.sqrMagnitude;

    if (movementSqrMagnitude > sqrMinimumExtent) 
    { 
        float movementMagnitude = Mathf.Sqrt(movementSqrMagnitude);
        //RaycastHit2D hitInfo; 

        //check for obstructions we might have missed 
        if (Physics2D.Raycast(previousPosition, movementThisStep, movementMagnitude, 0, layerMask.value)) 
            myRigidbody.position = (movementThisStep/movementMagnitude)*partialExtent;
        Debug.DrawLine(myRigidbody.position, myRigidbody.position - previousPosition, Color.green);
    } 

    previousPosition = myRigidbody.position; 
}

This is unitypackage https://www.dropbox.com/s/a3n1dalbc1k0k42/Hat%20Trick.unitypackage?dl=0

P.S. Sorry for my english and thank you for help!!

Upvotes: 3

Views: 2633

Answers (2)

Domi
Domi

Reputation: 24628

Explanation

Continuous collision detection in Unity does not use raycasting. As a result, very fast moving (and/or comparatively small) objects (let's call that kind of object projectile for now) still pass through things without a collision being detected. The famous DontGoThroughThings component fixes that for 3D. There are a few inconsistencies, but it can get the job done if you know what you are doing.

Here is my 2D adaption of it.

I added some features to make it more user-friendly to everyone who is not that good at coding or game physics.

How to use it

  1. Add this component to your fast moving objects and they will always trigger the OnTriggerEnter2D event when hitting something.
  2. You can also chose to send a different, custom message instead (by changing the MessageName variable). I actually recommend that due to the caveat explained below.
  3. Sending messages is the script's primary use case. It will not magically make a projectile behave correctly in a physical sense.
  4. The triggerTarget variable determines whether it sends the message to itself (in case, you have the hit handling script attached to the projectile), to the object being hit (in case, you have the hit handling attached to the objects that should be hit by projectiles), or any variation of the two.
  5. Unlike the original version, this script also allows for force to be applied upon impact which can be tuned through the momentumTransferFraction variable. When two objects collide the force generated is a result of a transfer of momentum (mass times velocity) between the two objects. The way I do this is very rudimentary and it is missing a lot of contributing factors, but it is enough to have projectiles push objects on impact. Just like in the real world, the faster or heavier your projectile is, the more force is exerted.

There are also some caveats (most of which also apply to the original version)

  1. Only apply on this on very fast moving objects. The less you use it the better because it is quite a bit more computationally expensive than the normal collision detection.
  2. While collision detection is more accurate, Collision resolution is only very rudimentary. It is not as good as what the physics engine does by default.
  3. In the current version, collisions are always detected in hindsight. That is why you might see projectiles having gone through the object by the time the collision is registered. I want to fix that in the near future.
  4. If you use this on objects like bullets or other forms of projectiles that basically stop working after first hit, you can set momentumTransferFraction to 1 to let the bullet physically push the object (by applying all its momentum to the first hit object) without the bullet being affected itself.
  5. For some reason, you cannot disable default collision detection just for one object. This means that if you are so (un-)lucky and a collision happens to be registered by Unity's default collision checks, you might have OnTriggerEnter2D fire multiple times on the same object, or (if collider is not a trigger) exert a force on hit targets (in addition to the one exerted by this script). However, since that would be somewhat random and very inconsistent, in addition to turning on IsTrigger on your projectile's collider, I recommend using a custom message name to handle projectile impacts. This way, collisions that are randomly detected by default collision detection would not have any unintended side effects [Remember that default collision detection being inconsistent for these sorts of objects is the actual reason why you add this script]. FYI: As of Unity 5, the only two ways to prevent default collision detection are IgnoreCollision and IgnoreLayerCollision.

Code

using UnityEngine;
using System.Collections;
using System.Linq;


/// <summary>
/// 2D adaption of the famous DontGoThroughThings component (http://wiki.unity3d.com/index.php?title=DontGoThroughThings).
/// Uses raycasting to trigger OnTriggerEnter2D events when hitting something.
/// </summary>
/// <see cref="http://stackoverflow.com/a/29564394/2228771"/>
public class ProjectileCollisionTrigger2D : MonoBehaviour {
    public enum TriggerTarget {
        None = 0,
        Self = 1,
        Other = 2,
        Both = 3
    }

    /// <summary>
    /// The layers that can be hit by this object.
    /// Defaults to "Everything" (-1).
    /// </summary>
    public LayerMask hitLayers = -1;

    /// <summary>
    /// The name of the message to be sent on hit.
    /// You generally want to change this, especially if you want to let the projectile apply a force (`momentumTransferFraction` greater 0).
    /// If you do not change this, the physics engine (when it happens to pick up the collision) 
    /// will send an extra message, prior to this component being able to. This might cause errors or unexpected behavior.
    /// </summary>
    public string MessageName = "OnTriggerEnter2D";

    /// <summary>
    /// Where to send the hit event message to.
    /// </summary>
    public TriggerTarget triggerTarget = TriggerTarget.Both;

    /// <summary>
    /// How much of momentum is transfered upon impact.
    /// If set to 0, no force is applied.
    /// If set to 1, the entire momentum of this object is transfered upon the first collider and this object stops dead.
    /// If set to anything in between, this object will lose some velocity and transfer the corresponding momentum onto every collided object.
    /// </summary>
    public float momentumTransferFraction = 0;

    private float minimumExtent;
    private float sqrMinimumExtent;
    private Vector2 previousPosition;
    private Rigidbody2D myRigidbody;
    private Collider2D myCollider;


    //initialize values 
    void Awake()
    {
        myRigidbody = GetComponent<Rigidbody2D>();
        myCollider = GetComponents<Collider2D> ().FirstOrDefault();
        if (myCollider == null || myRigidbody == null) {
            Debug.LogError("ProjectileCollisionTrigger2D is missing Collider2D or Rigidbody2D component", this);
            enabled = false;
            return;
        }

        previousPosition = myRigidbody.transform.position;
        minimumExtent = Mathf.Min(myCollider.bounds.extents.x, myCollider.bounds.extents.y);
        sqrMinimumExtent = minimumExtent * minimumExtent;
    }

    void FixedUpdate()
    {
        //have we moved more than our minimum extent? 
        var origPosition = transform.position;
        Vector2 movementThisStep = (Vector2)transform.position - previousPosition;
        float movementSqrMagnitude = movementThisStep.sqrMagnitude;

        if (movementSqrMagnitude > sqrMinimumExtent) {
            float movementMagnitude = Mathf.Sqrt(movementSqrMagnitude);

            //check for obstructions we might have missed 
            RaycastHit2D[] hitsInfo = Physics2D.RaycastAll(previousPosition, movementThisStep, movementMagnitude, hitLayers.value);

            //Going backward because we want to look at the first collisions first. Because we want to destroy the once that are closer to previous position
            for (int i = 0; i < hitsInfo.Length; ++i) {
                var hitInfo = hitsInfo[i];
                if (hitInfo && hitInfo.collider != myCollider) {
                    // apply force
                    if (hitInfo.rigidbody && momentumTransferFraction != 0) {
                        // When using impulse mode, the force argument is actually the amount of instantaneous momentum transfered.
                        // Quick physics refresher: F = dp / dt = m * dv / dt
                        // Note: dt is the amount of time traveled (which is the time of the current frame and is taken care of internally, when using impulse mode)
                        // For more info, go here: http://forum.unity3d.com/threads/rigidbody2d-forcemode-impulse.213397/
                        var dv = myRigidbody.velocity;
                        var m = myRigidbody.mass;
                        var dp = dv * m;
                        var impulse = momentumTransferFraction * dp;
                        hitInfo.rigidbody.AddForceAtPosition(impulse, hitInfo.point, ForceMode2D.Impulse);

                        if (momentumTransferFraction < 1) {
                            // also apply force to self (in opposite direction)
                            var impulse2 = (1-momentumTransferFraction) * dp;
                            hitInfo.rigidbody.AddForceAtPosition(-impulse2, hitInfo.point, ForceMode2D.Impulse);
                        }
                    }

                    // move this object to point of collision
                    transform.position = hitInfo.point;

                    // send hit messages
                    if (((int)triggerTarget & (int)TriggerTarget.Other) != 0 && hitInfo.collider.isTrigger) {
                        hitInfo.collider.SendMessage(MessageName, myCollider, SendMessageOptions.DontRequireReceiver);
                    }
                    if (((int)triggerTarget & (int)TriggerTarget.Self) != 0) {
                        SendMessage(MessageName, hitInfo.collider, SendMessageOptions.DontRequireReceiver);
                    }
                }
            }
        }

        previousPosition = transform.position = origPosition;
    }
}

Upvotes: 1

Lidan
Lidan

Reputation: 23

Here is the 2D version I rewrote of this script (for Unity 4.6):

using UnityEngine;
using System.Collections;

public class DontGoThroughThings : MonoBehaviour
{
    public delegate void CollidedDelegate(Collider2D collider);
    public event CollidedDelegate Collided;

    public LayerMask layerMask; //make sure we aren't in this layer 
    public float skinWidth = 0.1f; //probably doesn't need to be changed 

    private float minimumExtent;
    private float partialExtent;
    private float sqrMinimumExtent;
    private Vector2 previousPosition;
    private Rigidbody2D myRigidbody;



    //initialize values 
    void Awake()
    {
        myRigidbody = rigidbody2D;
        previousPosition = myRigidbody.transform.position;
        minimumExtent = Mathf.Min(BoundsOf(collider2D).extents.x, BoundsOf(collider2D).extents.y);
        partialExtent = minimumExtent * (1.0f - skinWidth);
        sqrMinimumExtent = minimumExtent * minimumExtent;
    }

    void FixedUpdate()
    {
        //have we moved more than our minimum extent? 
        Vector2 movementThisStep = (Vector2)myRigidbody.transform.position - previousPosition;
        float movementSqrMagnitude = movementThisStep.sqrMagnitude;

        if (movementSqrMagnitude > sqrMinimumExtent)
        {
            float movementMagnitude = Mathf.Sqrt(movementSqrMagnitude);

            //check for obstructions we might have missed 
            RaycastHit2D[] hitsInfo = Physics2D.RaycastAll(previousPosition, movementThisStep, movementMagnitude, layerMask.value);

            //Going backward because we want to look at the first collisions first. Because we want to destroy the once that are closer to previous position
            for (int i = hitsInfo.Length-1; i >= 0; i--)
            {
                var hitInfo = hitsInfo[i];
                if (hitInfo && hitInfo.rigidbody != rigidbody2D)
                {
                    if (Collided != null)
                    {
                        Collided(hitInfo.collider);
                    }
                }
            }
        }

        previousPosition = myRigidbody.transform.position;
    }

    // compute bounds in local space
    public static Bounds BoundsOf(Collider2D collider) {
        var bounds = new Bounds();

        var bc = collider as BoxCollider2D;
        if (bc) {
            var ext = bc.size * 0.5f;
            bounds.Encapsulate(new Vector3(-ext.x, -ext.y, 0f));
            bounds.Encapsulate(new Vector3(ext.x, ext.y, 0f));
            return bounds;
        }

        var cc = collider as CircleCollider2D;
        if (cc) {
            var r = cc.radius;
            bounds.Encapsulate(new Vector3(-r, -r, 0f));
            bounds.Encapsulate(new Vector3(r, r, 0f));
            return bounds;
        }


        // others :P
        //Debug.LogWarning("Unknown type "+bounds);

        return bounds;
    }

    // return bounds in world space
    public static Bounds BoundsColliders(GameObject obj) {
        var bounds = new Bounds(obj.transform.position, Vector3.zero);

        var colliders = obj.GetComponentsInChildren<Collider2D>();
        foreach(var c in colliders) {
            var blocal = BoundsOf(c);
            var t = c.transform;
            var max = t.TransformPoint(blocal.max);
            bounds.Encapsulate(max);
            var min = t.TransformPoint(blocal.min);
            bounds.Encapsulate(min);
        }

        return bounds;
    }


}

Please let me know if it works for you.

Thanks, Lidan

Upvotes: 0

Related Questions