3D camera for 2D game projectile not shoot to crosshair?

So I have a game in which the player can shoot projectiles towards a crosshair and this is a 2D game, however recently I switched the camera to perspective so that the background could have a parallax effect. The main items of this game are still all in one z value besides the background. When I shoot a bullet while moving, it appears to go either way above or way below the crosshair, meaning you essentially cannot aim while moving. When I switch back to orthographic camera this is not an issue. The icicle is instantiated and uses this code to angle it to the mouse:

Vector2 direction = Camera.main.ScreenToWorldPoint(Input.mousePosition) - transform.position;
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
Quaternion rotation = Quaternion.AngleAxis(angle, Vector3.forward);

The crosshair item uses this code in update:

crosshairImage.transform.position = Input.mousePosition;

Does anyone know why this would appear to go to the wrong place?

Now that you are using perspective camera, your problem is here:

Here’s why: ScreenToWorldPoint meaning of Z:

NOTE: you can use parallax with pure 2D (ortho camera) as well. You just need to move the background accordingly. There’s tutorials for how to do parallax in ortho mode.

I used to have orthographic camera and parallax script but decided to upgrade my project to perspective so I could do more interesting things
So I understand how ScreenToWorldPoint works but I’m not sure what you mean I should do to fix this problem… the Vector is taking the point and turning it into a Vector3 position in the world so it should go to that point no? You explained how screentoworldpoint has a z pos of 0 so should I in the next line set the z pos to something other than 0?

Yes, but it may be a teeny bit more complicated, depending on how you’re using that world point.

Just using the world point and a fixed Z will project it on a spherical curve in front of you, every point that distance from the camera in 3D space. The error gets bigger as you move away from the centerline of the camera.

Instead, make a Plane object (the struct, not the graphics primitive!), set it at origin: coplanar to your world (probably z == 0) and facing back, then raycast from the ray you get from the mouse.

Humorously I tried to see if ChatGPT was up to the task… after a few cycles it came to its senses:

// Generate a ray from the camera to the mouse position
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

// Calculate the intersection with the z == 0 plane
// We know the plane's normal (Vector3.forward) and point on the plane (z == 0)
float distance;
if (new Plane(Vector3.forward, Vector3.zero).Raycast(ray, out distance))
{
    Vector3 worldMousePosition = ray.GetPoint(distance);
    // worldMousePosition will be at z == 0
}

The only thing I would change above is to use named arguments for the two identical parameters to the Plane() constructor… I’m allergic to unnamed arguments of the same type.

So I’m not understanding what this plane is supposed to do with the Raycast. The Ray ray will go through the input.mousePosition and the point where it hits a plane that starts at 0, 0, 0, wouldn’t this ray still result in a z position of 0? I’m not particularly sure how the plane function works

From my reading of your post you actually want the mouse position on Z == 0, which is what an ortho camera gave you before, what a perspective camera can give you with the code above.

If the idea is to set the z pos of mousePosition vector to zero couldn’t I just:

Vector3 direction = screenToWorldPoint(Input.mousePosition);
direction = new Vector3(direction.x, direction.y, 0);

or am I not understanding correctly?

Try it with a perspective camera. It will always return the x/y of the camera itself, regardless of mouse position.

Add more coffee (perhaps tomorrow morning?) and carefully re-read the link I posted above:

Go ahead, try it yourself with a perspective camera:

var pos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
pos.z = 0;
cube.transform.position = pos;

If you pass a fixed Z in, you get the inside of a curved surface exactly Z away from the camera.

That’s not a plane at z == 0. That’s the inside of a sphere. If you smash z flat to 0, it will have more error the more distant you are from the camera’s centerline.

The Plane code a few posts back give you an on-Plane-z==0 position

your main misunderstanding is that what you click is essentially the Z of the camera

so your characters are moving on z = 0, your camera is probably on z = -10, and so it will give a different position than what you clicked

It happens for the same reason that your paralax effects work, the position is displaced based on the distance from the camera and depth of field to a given position far away

I tried putting in your code for the plane where I instantiate it:

            var projectile = Instantiate(object, transform.position + spawnPoint, transform.rotation);
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            // Calculate the intersection with the z == 0 plane
            // We know the plane's normal (Vector3.forward) and point on the plane (z == 0)
            float distance;
            if (new Plane(Vector3.forward, Vector3.zero).Raycast(ray, out distance))
            {
                Vector3 worldMousePosition = ray.GetPoint(distance);
                Vector3 direction = worldMousePosition - transform.position;
                direction = new Vector3(direction.x, direction.y, transform.position.z);
                float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
                Quaternion rotation = Quaternion.AngleAxis(angle, Vector3.forward);
                projectile.transform.rotation = rotation;   
                projectile.GetComponent<Rigidbody2D>().AddForce(direction.normalized * shootforce);
            }

And I’m not particularly understanding what was the distance float supposed to do because it was never used in the plane?
The crosshair/projectile still seem to not line up to the same place…

No idea what you’re trying to do here but you cannot ask a 2D Rigidbody to move in 3D which is what you’re doing here. Direction is a Vector3, it requires a Vector2. You’ve set Z which is supposed to be a direction to an absolute Z position which makes no sense. This wouldn’t be a problem but you first normalize the Vector3 which will adjust it to be a unit-normal but that includes the Z position so the XY will be wrong.

Also, just stomping over the Transform AND asking a Rigidbody2D to be in control of it is wrong. Use the Rigidbody2D API and let it update the transform.

projectile.GetComponent<Rigidbody2D>().AddForce(((Vector2)direction).normalized * shootforce);

Would this work instead?
My game is a 2D game where everything is on z==0 layer except for the camera and as such, the crosshair and canvas

I’m not sure what lines 9 onwards are but let’s focus on the question at hand.

You have changed to using a perspective camera, which breaks the previous screen-to-world usage, which relied on an orthographic camera.

To cast a perspective camera, which sees in a cone, to a flat surface, the Plane-cast code is necessary.

And as for this:

Read the code again. It is an out product from the Raycast and it is used in the .GetPoint() method. If any of that remotely sounds confusing, go to the documentation.

So finally, all wrapped up, using the code above for plane-casting, test ONLY THIS so you know the mouse position is correct on your z == 0 plane.

One problem at a time please. Let’s at least get the 2D positional data correct BEFORE we go into crazy Atan2() aiming stuff. :slight_smile:

using UnityEngine;
using UnityEngine.InputSystem;

// @kurtdekker - to demo, put this on a blank GameObject in front of a perspective Camera
//
// makes a tiny cube that tracks the z == 0 plane from a perspective Camera.

public class Test1 : MonoBehaviour
{
	GameObject cube;

	private void Start()
	{
		cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
		cube.transform.localScale = Vector3.one * 0.2f;
	}

	void Update()
	{
		// this is using the NEW input system; comment this next line
		// out and replace with the next line after this if you are
		// using the OLD input system!!
		Vector2 mousePos = Mouse.current.position.value;

		// only for OLD input system...
//		Vector2 mousePos = Input.mousePosition;

		Ray ray = Camera.main.ScreenPointToRay(mousePos);

		float distance;
		if (new Plane(
			inNormal:Vector3.forward,
			inPoint:Vector3.zero).Raycast(ray, out distance))
		{
			Vector3 worldMousePosition = ray.GetPoint(distance);

			// worldMousePosition will be at z == 0

			cube.transform.position = worldMousePosition;
		}
	}
}

So I have inserted this code into a new project and it works with a cube being set to the mouse pos with a z of 0… how would I use this to fix the issue? Set the value that the projectile is rotated towards to be this worldMousePos value?

Subtract the projectile’s origin (or player’s origin) from the projected mouse position.

This gives you the “to” vector.

Then use that as follows:

You may be able to simply drive transform.forward (or transform.up or transform.right when in 2D) equal to your movement vector.

Otherwise, use Mathf.Atan2() to derive heading from cartesian coordinates:

Sine/Cosine/Atan2 (sin/cos/atan2) and rotational familiarity and conventions

So I have this as my code for the projectile and this should Instantiate it and move it towards this direction

Vector2 mousePos = Input.mousePosition;
            Ray ray = Camera.main.ScreenPointToRay(mousePos);

		    float distance;
		    if (new Plane(inNormal:Vector3.forward,inPoint:Vector3.zero).Raycast(ray, out distance))
		    {
			Vector3 worldMousePosition = ray.GetPoint(distance);

			// worldMousePosition will be at z == 0
                     var projectile = Instantiate(icicle, transform.position + spawnPoint, transform.rotation);
			Vector2 direction = worldMousePosition - projectile.transform.position;
            projectile.GetComponent<Rigidbody2D>().AddForce(direction.normalized * shootforce);

		    }

But he said that using rigidbody2d with this would not work so would I need to use rigidbody 3d for all of my objects even though it is meant to be a 2d game in 3d space?