Search code examples
c#unity-game-enginegame-physics

Adding force to LineRenderer relative to the length of the line?


I've lurked around here for ages and found a lot of help through other people's questions. But after extensive searching, I couldn't find a post to help me find a solution to a problem that I'm having, so I decided to post my own question here so if it gets answered, it might help someone else in a similar fashion!

I'm having some issues with adding force to my linerenderer. The basic idea is that I have an object that I shoot forward by dragging a line in the opposite direction. Now all that is missing is for the shot to have force that is relative to the length of the "dragline", as in how "strong" the object is pulled. I'm having some trouble with that so I thought I'd ask someone smarter and more experienced than me!

Here is my script to draw the dragline:

public class DragIndicatorScript : MonoBehaviour

{

    Vector3 startPos;
    Vector3 endPos;
    Camera camera;
    LineRenderer lineRenderer;

    Vector3 camOffset = new Vector3(0, 0, 10);

    [SerializeField] AnimationCurve animCurve;

    // Start is called before the first frame update
    void Start()
    {
        camera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (lineRenderer == null)
            {
                lineRenderer = gameObject.AddComponent<LineRenderer>();
            }
            lineRenderer.enabled = true;
            lineRenderer.positionCount = 2;
            startPos = camera.ScreenToWorldPoint(Input.mousePosition) + camOffset;
            lineRenderer.SetPosition(0, startPos);
            lineRenderer.useWorldSpace = true;
            lineRenderer.widthCurve = animCurve;
            lineRenderer.numCapVertices = 10;
            lineRenderer.material = new Material(Shader.Find("Hidden/Internal-Colored"));
            lineRenderer.startColor = Color.magenta;
            lineRenderer.endColor = Color.cyan;
        }
        if (Input.GetMouseButton(0))
        {
            endPos = camera.ScreenToWorldPoint(Input.mousePosition) + camOffset;
            lineRenderer.SetPosition(1, endPos);
        }
        if (Input.GetMouseButtonUp(0))
        {
            lineRenderer.enabled = false;
        }
    }
}

and here is my script to control the shooting of the object:

public class Shoot : MonoBehaviour
{
    Rigidbody2D rigidigidy;
    Vector2 startpos;
    Vector2 endpos;
    [SerializeField]  float power = 10f;

    void Start()
    {
        rigidigidy = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        if (Input.GetMouseButtonUp(0))
        {
            endpos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            Launch();
        }
    }

    void OnMouseDown()
    {
        startpos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
    }

    void Launch()
    {
        Vector2 direction = (startpos - endpos).normalized; 
        rigidigidy.AddForce(direction * power, ForceMode2D.Impulse);
    }
}

Thanks in advance! I have used Google for hours on end and written several lines of code and I feel like I just can't find what I'm looking for.

EXTRA QUESTION 1) How do I clamp the linerenderer to always be where the pullable object is? I wish to make it so that the player can drag anywhere on the playable area, but the linerenderer always instantiates on the object in question (it is always the same object, as in it is the "player model").

EXTRA QUESTION 2) I know how to change the material of the linerenderer inside the script, but I haven't really found a proper way to call the material from my assets (I made a material that I would like to use as the dragline), so as a placeholder I am just using:

Material(Shader.Find("Hidden/Internal-Colored"));
            lineRenderer.startColor = Color.magenta;
            lineRenderer.endColor = Color.cyan;

since I wanted to have something there that is even a little more pleasing to the eye than just the default pink one.


Solution

    1. The main question is a bit unclear to me. You already have the startpos-endpos. Wouldn't this already be the vector including the magnitude you are looking for?

      I would probably rather combine your two scripts and not let them handle input and positions independently.

      I would let your Shoot handle the input and only forward it to the DragIndicatorScript since as the name suggests: It shall only be an indicator and should not be the component deciding the final shoot strength.

      So the Shoot should rather calculate the input vector, clamp it and only pass this to the indicator which then can update its end position based on the anchor (see below) and the vector.

    2. EXTRA QUESTION 1:

      I would simply remember the offset between the anchor and the startPos. Then you can later calculate back the endPoint by simply taking this offset into account.

      For being able to drag anywhere on the screen you should not use OnMouseDown which only works if the mouse is actually over your collider but rather go the way you did in DragIndicatorScript and use GetMouseButtonDown(0).

      It sounds like you additionally also want a maximum length for the line and use this maximum length also as maximum shoot power. One argument more as said before why I would let shoot control it all.

    3. EXTRA QUESTION 2:

      You can simply reference your material in a slot in the Inspector. In general however I would suggest to rather add the LineRenderer already beforehand and completely set it up. Then you don't even need to do it via code!

    So it might look somewhat like

    public class Shoot : MonoBehaviour
    {
        [SerializeField] private DragIndicatorScript _dragIndicator;
    
        [SerializeField] private Rigidbody2D _rigidBody;
        [SerializeField] private float power = 10f;
    
        [SerializeField] private Camera _camera;
    
        // This script should clamp the input
        [SerializeField] private float maxLength;
    
        private Vector2 inputStartPosition;
    
        private void Awake()
        {
            if(!_rigidBody) _rigidBody = GetComponent<Rigidbody2D>();
            if(!_camera) _camera = Camera.main;
        }
    
        private void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                // store the initial input position
                inputStartPosition = camera.ScreenToWorldPoint(Input.mousePosition);
    
                // Enable the indicator and for now set the endpoint also on the anchor since there wasn't any input yet
                _dragIndicator.SetIndicator(Vector2.zero);
            }
            if (Input.GetMouseButton(0))
            {
                // Get the current position
                Vector2 inputCurrentPosition = camera.ScreenToWorldPoint(Input.mousePosition);
    
                // Get the input delta between the current and initial position
                var input = inputCurrentPosition - inputStartPosition;
    
                // Now clamp this vector to the max length
                input = Vector2.ClampMagnitude(input, maxLength);
    
                _dragIndicator.SetIndicator(input);
            }
            if (Input.GetMouseButtonUp(0))
            {
                // Just to be sure recalculate from the current position
                Vector2 inputCurrentPosition = camera.ScreenToWorldPoint(Input.mousePosition);
                var input = inputCurrentPosition - inputStartPosition;
    
                // Now clamp this to the max length
                input = Vector2.ClampMagnitude(input, maxLength);
                
                _dragIndicator.HideIndicator();
    
                // Directly pass in the final input vector, this way you don't need to store it in a field
                Launch(input);
            }
        }
    
        private void Launch(Vector2 input)
        {
            // Input already contains the direction and magnitude of the user input
            _rigidBody.AddForce(-input * power, ForceMode2D.Impulse);
        }
    }
    

    and the DragIndicatorScript only receives these commands:

    public class DragIndicatorScript : MonoBehaviour
    {
        [SerializeField] private LineRenderer _lineRenderer;
    
        // For the EXTRA QUESTION 1
        [SerializeField] private Transform _lineAnchor;
    
        // For EXTRA QUESTION 2
        [SerializeField] private Material _lineMaterial;
    
        [SerializeField] private AnimationCurve animCurve;
    
        private void Awake()
        {
            if(!TryGetComponent<LineRenderer>(out _lineRenderer))
            {
                _lineRenderer = gameObject.AddComponent<LineRenderer>();
            }
    
            // Setup your Line ONCE
            _lineRenderer.positionCount = 2;
            _lineRenderer.useWorldSpace = true;
            _lineRenderer.widthCurve = animCurve;
            _lineRenderer.numCapVertices = 10;
            _lineRenderer.material = _lineMaterial;
            _lineRenderer.startColor = Color.magenta;
            _lineRenderer.endColor = Color.cyan;
    
            _lineRenderer.SetPosition(0, _lineAnchor.position);
            _lineRenderer.enabled = false;
        }
    
        public void HideIndicator()
        {
            _lineRenderer.enabled = false;
        }
    
        public void SetIndicator(Vector3 input)
        {
            lineRenderer.SetPosition(1, _lineAnchor.position + input);
            _lineRenderer.enabled = true;
        } 
    }