How to calculate cursor delta like in FPS games? - c#

I'm making an XNA game and I'd like the cursor to stop when the mouse button is held down. Right now by setting the cursor position every 1/60 of a second to a certain location makes the cursor look laggy and sometimes if the cursor moves fast enough to reach a screen border, from that moment and until the next position reset the delta position is not calculated correctly. Also, starting at the edge of the screen and moving towards that same edge won't get any correct deltas.
How do I get the correct cursor deltas and not get bummed by cursor stopping at screen edges?

If the position you are locking the mouse to is too close to the edge, you are out of luck.
I suggest you hide the cursor at the beginning of the drag motion, save the location and secretly move the cursor to the center of the screen. When dragging ends just restore the cursor to its previous location.

Easily, supposing I understood what you meant.
In order to move a 1st person camera according to the mouse, we need to calculate the delta each frame, and then move the mouse back to the center of the screen.
To do so we will store the location of the center of the screen. We also need a MouseState variable and a yaw and a pitch floats to store our current camera rotations. I also like to set the rotation speed according the game's resolution:
MouseState currentMouseState;
Vector2 screenCenter;
float YrotationSpeed;
float XrotatiobSpeed;
float yaw = 0;
float pitch = 0;
...
protected override void LoadContent()
{
YrotationSpeed = (float)graphics.PreferredBackBufferWidth / 10000.0f;
XrotatiobSpeed = (float)graphics.PreferredBackBufferHeight / 12000.0f;
screenCenter = new Vector2(graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height) / 2;
}
Now, to calculate how much did the mouse move in the last frame, we will use this function:
private void HandleMouse(GameTime gameTime)
{
currentMouseState = Mouse.GetState();
float amount = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f;
if (currentMouseState.X != screenCenter.X)
{
yaw -= YrotationSpeed * (currentMouseState.X - screenCenter.X) * amount;
}
if (currentMouseState.Y != screenCenter.Y)
{
pitch -= XrotatiobSpeed * (currentMouseState.Y - screenCenter.Y) * amount;
if (pitch > MathHelper.ToRadians(90))
pitch = MathHelper.ToRadians(90);
if (pitch < MathHelper.ToRadians(-50)) //Preventing the user from looking all the way down (and seeing the character doesn't have any legs..)
pitch = MathHelper.ToRadians(-50);
}
Mouse.SetPosition((int)screenCenter.X, (int)screenCenter.Y);
}
Finally:
To update the camera and view Matrix, you could use the following function, that should be executed after we update our yaw and pitch:
public void UpdateCamera(float yaw, float pitch, Vector3 position)
{
Matrix cameraRotation = Matrix.CreateRotationX(pitch) * Matrix.CreateRotationY(yaw);
Vector3 cameraOriginalTarget = new Vector3(0, 0, -1);
Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation);
Vector3 cameraFinalTarget = position + cameraRotatedTarget;
Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0);
Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation);
view = Matrix.CreateLookAt(position, cameraFinalTarget, cameraRotatedUpVector);
}
EDIT: Well, as I re-read the question I had realized my answer isn't quite what you were asking for. Nevertheless, it does show how to calculate the mouse delta without it getting to the borders of the screen.
To match my answer to your question, all you need are tiny adjustments.
By the way, unless you are willing to draw an image of a mouse in the middle of the screen, any methood WILL look kinda laggy.
Therefore, I highly advice you to hide the cursor.
To do so, just add this line:
this.IsMouseVisible = false;

Related

wheel collider steering in new input management

I'm trying to use the new 3D drive input system for my car's movement, more specifically the steering angle.
when i use Input.GetAxis("Horizontal") my wheel goes smoothly from 0 to 30 and as i press keys.
when i use context.ReadValue() it jumps from 0 to 30 with a click and returns to 0 when released without being smooth.
I can't get the wheel to rotate at the angle according to the key press and come back smoothly like in the old system, can anyone help me?
Inputmanage input;
public float currentangle = 0f;
public float angle = 30f;
public void Onmovement(InputAction.CallbackContext context) {
currentangle = angle * context.ReadValue<float>();
wheels[0].steerAngle = currentangle;
wheels[1].steerAngle = currentangle;
}

Calculating changing Mathf.Clamp() values for object of changing size

I've been wracking my brain over this problem for a while. I have a controllable object (only moveable up and down) that I have clamped on the top and bottom. However, every time a certain trigger is activated, the object shrinks 10% until it is 90% of the original size. The problem comes in when I shrink it. When it shrinks, the min and max values of the clamp don't change at all and this results in the object clamping to soon on the min and max. They need to decrease and increase respectively so that it clamps in the exact same place. How would one do this? I've tried so many different methods but to no avail...
This is the way I shrink my player:
playerWidth /= 1.1f;
playerHeight /= 1.1f;
Vector3 scale = new Vector3(playerWidth, playerHeight, 1f);
playerOneShrink.transform.localScale = scale;
Many Thanks!
EDIT 1:
Let me give an example using a game that might help get my point across better. Basically think flappy bird, but the player uses the up and down arrows to move and not space-bar. When the bird dies, the bird would reset but become smaller. It seems that because the bird is smaller it now moves 'less' up and down, and doesn't touch the top and bottom of the screen anymore. I want the bird to still be touching the top and bottom of the screen.
EDIT 2:
So apparently my first edit didn't clarify as much as I thought so I'm going to show my code for a test scene I'm trying this all in. I haven't shown the changing of minValue and maxValue because that's what I'm struggling with...
private static Rigidbody2D playerOneRB;
GameObject player1;
float verticalMovement = 0;
public static float playerHeight = 0.3417f;
public static float playerWidth = 0.05400001f;
public float minValue = 0;
public float maxValue = 10;
public static float p1VerticalSpeed = 3;
public GameObject playerOneShrink;
// Start is called before the first frame update
void Start()
{
playerOneRB = GameObject.FindGameObjectWithTag("P1 Normal").GetComponent<Rigidbody2D>();
player1= GameObject.Find("P1 Normal");
}
// Update is called once per frame
void Update()
{
verticalMovement = Input.GetAxis("Player 1 V"); //Looking for axis defined in our input manager
playerOneRB.velocity = new Vector3(0f, verticalMovement * p1VerticalSpeed, playerOneRB.velocity.x);
Vector3 scale1 = new Vector3(playerWidth, playerHeight, 1f);
Vector3 position = transform.position;
player1.transform.position = new Vector3(player1.transform.position.x, (Mathf.Clamp(player1.transform.position.y, minValue, maxValue)), 0f); //Restricting how far the players can move (aka, not off the screen)
if (Input.GetKeyDown(KeyCode.Alpha1))
{
playerWidth /= 1.1f;
playerHeight /= 1.1f;
scale1 = new Vector3(playerWidth, playerHeight, 1f);
paddleOneShrink.transform.localScale = scale1;
}
}
Here's a photo of what's happening:
1. Photo of position it reaches before shrinking
2. Photo of position it reaches after shrinking
I want the player in image 2 to be able to get to the same top position as in image 1 after it shrinks.
It sounds like you want to scale the object, but keep it pivoted, so if it's at the top most position, and scales, it stays at the top most position? If so, the code and answers here will help:
https://answers.unity.com/questions/14170/scaling-an-object-from-a-different-center.html
You'll of course need to add in some conditional code to check whether it's a top or bottom pivot.
Hope I understood your problem.
So let's assume the scale of the player is 1 unit and you have to clamp in y axis between 0 and 10.
so you will have to clamp the Y position of player like
Vector3 scale = new Vector3(playerWidth, playerHeight, 1f);
Vector3 position = transform.position;
position.y = mathf.clamp(0 + scale.x ,10 - scale.x ,position.y );
transform.position = position;
so if the player's scale is 1 then it will limit the y position between 1 (0+1) and 9 (10-1).
if the player's scale is 0.9 then it will be between 0.9 (0+0.9 ) and 9.1 (10-0.9)
if the scale is 3 then between 3 (0+3) and 7 (10-3)

How to create a 180 degree camera orbit over X seconds in Unity?

I have a camera that orbits a point on the screen. Clicking and dragging your mouse left and right will orbit around the focus point and releasing the mouse will leave the camera at that angle.
I'm trying to create a function that will orbit the camera 180 degrees, smoothly. So for example, if the camera is behind the "player" and this function gets called, the camera will orbit to the front of the player over the course of X seconds and stop in the exact opposite position of where it started.
Here's the code I'm currently using for camera rotation based on mouse click and drag:
Quaternion camTurnAngle =
Quaternion.AngleAxis(Input.GetAxis("Mouse X") * rotationSpeed, Vector3.up);
this.cameraOffset = camTurnAngle * this.cameraOffset;
Vector3 newPos = this.currentFocusPoint + this.cameraOffset;
transform.position = Vector3.Slerp(transform.position, newPos, smoothFactor);
transform.LookAt(this.currentFocusPoint);
I am trying to replace this mouse-drag based camera rotate with a UI button (aka, a single function call) that will fully orbit the camera 180 degrees over a short period of time, and then it will rotate back if pressed again. Here's an MS paint drawing just in case my explanation is confusing:
I'm not sure how do to something like this. It sounds similar to using Vector3.Slerp, but I cannot find a solution that works. I am so far from a solution that I don't really have any sensible code to post. I'll just say that I've tried two methods so far:
1) I've tried using transform.RotateAround, where angle I pass in is rotateSpeed * Time.deltaTime. My issue is I don't know how to check for an end condition, and without one my camera will rotate forever.
2) I've tried messing with Quaternion.Slerp, but the results are seeing are not what I expected.
How can I achieve a smooth 180 degree camera orbit, that takes a predetermined amount of time to complete?
This is a textbook use for a coroutine. Basically, start a coroutine that keeps track of how much time is left in the orbit, then either updates cameraOffset based on the time that's left or Time.deltaTime, whatever is shorter.
private IEnumerator RotateCam(float rotateDuration)
{
float timeLeft = rotateDuration;
// possibly - disable control of camera here
while (timeLeft > 0)
{
yield return null;
float elapsedRotationTime = Mathf.Min(timeLeft, Time.deltaTime);
float angleThisFrame = 180f * elapsedRotationTime / rotateDuration;
Quaternion camTurnAngle = Quaternion.AngleAxis(angleThisFrame, Vector3.up);
this.cameraOffset = camTurnAngle * this.cameraOffset;
transform.position = this.currentFocusPoint + this.cameraOffset;
transform.LookAt(this.currentFocusPoint);
timeLeft -= Time.deltaTime;
}
// possibly - re-enable control of camera here
}
Then, when you want to begin the rotation:
// private Coroutine rotateCoroutine
this.rotateCoroutine = StartCoroutine(RotateCam(2f));

How to make rts camera smooth when zooming in Unity

right now I am trying to make rts camera zooming with panning when close to the ground. The problem I have now is that I use mouse scroll wheel for zooming and it makes the zooming feel like it's laggy. It looks like it jumps some Y value and teleports rather than smoothly move to the desired position. Also, I would like to know how to make the camera stop at the minimum Y value because what is happening now is that it stops at around 22 rather than 20 which is my minimum Y value for the camera movement.
I tried zooming with + and - on my numpad and it worked how I wanted (smoothly zooming in and out without skipping) but not all players have numpad and I feel it is more suitable to zoom with mouse wheel.
{
private const int levelArea = 100;
private const int scrollArea = 25;
private const int scrollSpeed = 25;
private const int dragSpeed = 70;
private const int zoomSpeed = 50;
// Maximum/minimum zoom distance from the ground
public int zoomMin = 20;
public int zoomMax = 120;
private const int panSpeed = 40;
// Minimal/maximal angles for camera
private const int panAngleMin = 30;
private const int panAngleMax = 90;
void Update()
{
// Init camera translation for this frame.
var translation = Vector3.zero;
// Zoom in or out
var zoomDelta = Input.GetAxis("Mouse ScrollWheel") * zoomSpeed * Time.deltaTime;
if (zoomDelta != 0)
{
translation -= Vector3.up * zoomSpeed * zoomDelta;
}
// Start panning camera if zooming in close to the ground or if just zooming out.
var pan = transform.eulerAngles.x - zoomDelta * panSpeed;
pan = Mathf.Clamp(pan, panAngleMin, panAngleMax);
// When to start panning up the camera
if (zoomDelta < 0 || transform.position.y < (zoomMax -20))
{
transform.eulerAngles = new Vector3(pan, 0, 0);
}
// Move camera with arrow keys
translation += new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
// Move camera with mouse
if (Input.GetMouseButton(2)) // MMB
{
// Hold button and drag camera around
translation -= new Vector3(Input.GetAxis("Mouse X") * dragSpeed * Time.deltaTime, 0,
Input.GetAxis("Mouse Y") * dragSpeed * Time.deltaTime);
}
else
{
// Move camera if mouse pointer reaches screen borders
if (Input.mousePosition.x < scrollArea)
{
translation += Vector3.right * -scrollSpeed * Time.deltaTime;
}
if (Input.mousePosition.x >= Screen.width - scrollArea)
{
translation += Vector3.right * scrollSpeed * Time.deltaTime;
}
if (Input.mousePosition.y < scrollArea)
{
translation += Vector3.forward * -scrollSpeed * Time.deltaTime;
}
if (Input.mousePosition.y > Screen.height - scrollArea)
{
translation += Vector3.forward * scrollSpeed * Time.deltaTime;
}
}
// Keep camera within level and zoom area
var desiredPosition = transform.position + translation;
if (desiredPosition.x < -levelArea || levelArea < desiredPosition.x)
{
translation.x = 0;
}
if (desiredPosition.y < zoomMin || zoomMax < desiredPosition.y)
{
translation.y = 0;
}
if (desiredPosition.z < -levelArea || levelArea < desiredPosition.z)
{
translation.z = 0;
}
// Move camera parallel to world axis
transform.position += translation;
}
}
I would like to have a smooth transition from the position where the camera is now and the desired position after scrolling in/out. And also I would like to know how to make the camera to stop at the minimum/maximum zoom distance rather than to stop close to it. Thank you for helping. Video how the camera movement looks like: https://youtu.be/Lt3atJEaOjA
Okay, so I'm going to do three things here. First and foremost, I'm going to recommend that if you're working with advanced camera behavior, you probably want to at least consider using Cinemachine. I'd walk you through it myself, but given my lack of personal experience with it, I'd probably be doing you a disservice by even trying. There are many good tutorials out there. Youtube and Google should provide.
The second thing I'll do is solve your problem in the most direct way I can manage, and after that, we'll see if we can't come up with a better method for solving your problem.
So, the key here is that Unity's scrollwheel input is pretty binary. When you check a scrollwheel axis, the result is directly based on how many "clicks" your wheel has gone through since the last frame update, but what you really want is something with a bit of give. By default, Unity can actually do this with most of its axis inputs: You might notice that if you use WASD in a default Unity project, there's a sort of "lag" to it, where you'll take your fingers off the keys but you'll still keep receiving positive values from Input.GetAxis() for a few frames. This is tied to the Gravity value in your input settings, and Input.GetAxisRaw() is actually used to circumvent this entirely. For whatever reason, scrollwheel axes don't seem to be affected by axis gravity, so we essentially have to implement something similar ourselves.
// Add this property to your class definition (so it persists between updates):
private float wheelAxis = 0;
// Delete this line:
var zoomDelta = Input.GetAxis("Mouse ScrollWheel") * zoomSpeed * Time.deltaTime;
// And put these three new lines in its place:
wheelAxis += Input.GetAxis("Mouse ScrollWheel");
wheelAxis = Mathf.MoveTowards(wheelTotal, 0f, Time.deltaTime);
var zoomDelta = Mathf.Clamp(wheelAxis, -0.05f, 0.05f) * zoomSpeed * Time.deltaTime;
Right, so we do a few things here. Every update, we add the current scrollwheel values to our wheelAxis. Next, we apply the current Time.deltatime as "gravity" via the Mathf.MoveTowards() function. Finally, we call what's mostly just your old zoomDelta code with a simple modification: We constrain the wheelAxis with Mathf.Clamp to try and regulate how fast the zooming can happen.
You can modify this code in a couple of ways. If you multiply the Time.deltaTime parameter you can affect how long your input will "persist" for. If you mess with the Mathf.Clamp() values, you'll get faster or slower zooming. All in all though, if you just want a smooth zoom with minimal changes to your code, that's probably your best bet.
So!
Now that we've done that, let's talk about your code, and how you're approaching the problem, and see if we can't find a somewhat cleaner solution.
Getting a good camera working is surprisingly non-trivial. Your code looks like a lot of code I see that tries to solve a complex problem: It looks like you added some feature, and then tested it, and found some edge cases where it fell apart, and then patched those cases, and then tried to implement a new feature on top of the old code, but it kinda broke in various other ways, etc etc, and what we've got at this point is a bit messy.
The biggest issue with your code is that the camera's position and the camera's rotation are closely tied together. When working with characters, this is usually fine, but when working with a camera, you want to break this up. Think about where the camera is and what the camera is looking at as very separate things to keep track of.
So here's a working camera script that you should be able to plug in and just run with:
using UnityEngine;
public class RTSCamera : MonoBehaviour
{
public float zoomSpeed = 100f;
public float zoomTime = 0.1f;
public float maxHeight = 100f;
public float minHeight = 20f;
public float focusHeight = 10f;
public float focusDistance = 20f;
public int panBorder = 25;
public float dragPanSpeed = 25f;
public float edgePanSpeed = 25f;
public float keyPanSpeed = 25f;
private float zoomVelocity = 0f;
private float targetHeight;
void Start()
{
// Start zoomed out
targetHeight = maxHeight;
}
void Update()
{
var newPosition = transform.position;
// First, calculate the height we want the camera to be at
targetHeight += Input.GetAxis("Mouse ScrollWheel") * zoomSpeed * -1f;
targetHeight = Mathf.Clamp(targetHeight, minHeight, maxHeight);
// Then, interpolate smoothly towards that height
newPosition.y = Mathf.SmoothDamp(transform.position.y, targetHeight, ref zoomVelocity, zoomTime);
// Always pan the camera using the keys
var pan = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")) * keyPanSpeed * Time.deltaTime;
// Optionally pan the camera by either dragging with middle mouse or when the cursor touches the screen border
if (Input.GetMouseButton(2)) {
pan -= new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y")) * dragPanSpeed * Time.deltaTime;
} else {
var border = Vector2.zero;
if (Input.mousePosition.x < panBorder) border.x -= 1f;
if (Input.mousePosition.x >= Screen.width - panBorder) border.x += 1f;
if (Input.mousePosition.y < panBorder) border.y -= 1f;
if (Input.mousePosition.y > Screen.height - panBorder) border.y += 1f;
pan += border * edgePanSpeed * Time.deltaTime;
}
newPosition.x += pan.x;
newPosition.z += pan.y;
var focusPosition = new Vector3(newPosition.x, focusHeight, newPosition.z + focusDistance);
transform.position = newPosition;
transform.LookAt(focusPosition);
}
}
While I encourage you to go through it in your own time, I'm not going to drag you through every inch of it. Instead, I'll just go over the main crux.
The key idea here is that rather than controlling the camera's height and orientation directly, we just let the scrollwheel dictate where want the camera's height to be, and then we use Mathf.SmoothDamp() to move the camera smoothly into that position over several frames. (Unity has many useful functions like this. Consider Mathf.MoveTowards() for an alternative interpolation method.) At the very end, rather than trying to fiddle with the camera's rotation values directly, we just pick a point in front of us near the ground and point the camera at that spot directly.
By keeping the camera's position and orientation completely independent of each other, as well as separating out the "animation" of the camera's height, we avoid a lot of headaches and eliminate a lot of potential for messy interwoven bugs.
I hope that helps.
Cinemachine is pretty good, but I suggest people learn on your own quite a bit before using Cinemachine so that you better understand what's going on in the background.

How to convert new Canvas ui image position to linerenderer position?

I have an image UI in a canvas with Screen Space - Camera render mode. What I like to do is move my LineRenderer to the image vertical position by looping through all the LineRenderer positions and changing its y axis. My problem is I cant get the correct position of the image that the LineRenderer can understand. I've tried using ViewportToWorldPoint and ScreenToWorldPoint but its not the same position.
Vector3 val = Camera.main.ViewportToWorldPoint(new Vector3(image.transform.position.x, image.transform.position.y, Camera.main.nearClipPlane));
for (int i = 0; i < newListOfPoints.Count; i++)
{
line.SetPosition(i, new Vector3(newListOfPoints[i].x, val.y, newListOfPoints[i].z));
}
Screenshot result using Vector3 val = Camera.main.ScreenToWorldPoint(new Vector3(image.transform.localPosition.x, image.transform.localPosition.y, -10));
The green LineRenderer is the result of changing the y position. It should be at the bottom of the square image.
Wow, this was annoying and complicated.
Here's the code I ended up with. The code in your question is the bottom half of the Update() function. The only thing I changed is what was passed into the ScreenToWorldPoint() method. That value is calculated in the upper half of the Update() function.
The RectTransformToScreenSpace() function was adapted from this Unity Answer post1 about getting the screen space coordinates of a RectTransform (which is exactly what we want in order to convert from screen space coordinates back into world space!) The only difference is that I was getting inverse Y values, so I changed from Screen.height - transform.position.y to just transform.position.y which did the trick perfectly.
After that it was just a matter of grabbing that rectangle's lower left corner, making it a Vector3 instead of a Vector2, and passing it back into ScreenToWorldPoint(). The only trick there was because of the perspective camera, I needed to know how far away the line was from the camera originally in order to maintain that same distance (otherwise the line moves up and down the screen faster than the image). For an orthographic camera, this value can be anything.
void Update () {
//the new bits:
float dist = (Camera.main.transform.position - newListOfPoints[0]).magnitude;
Rect r = RectTransformToScreenSpace((RectTransform)image.transform);
Vector3 v3 = new Vector3(r.xMin, r.yMin, dist);
//more or less original code:
Vector3 val = Camera.main.ScreenToWorldPoint(v3);
for(int i = 0; i < newListOfPoints.Count; i++) {
line.SetPosition(i, new Vector3(newListOfPoints[i].x, val.y, newListOfPoints[i].z));
}
}
//helper function:
public static Rect RectTransformToScreenSpace(RectTransform transform) {
Vector2 size = Vector2.Scale(transform.rect.size, transform.lossyScale);
Rect rect = new Rect(transform.position.x, transform.position.y, size.x, size.y);
rect.x -= (transform.pivot.x * size.x);
rect.y -= ((1.0f - transform.pivot.y) * size.y);
return rect;
}
1And finding that post from a generalized search on "how do I get the screen coordinates of a UI object" was not easy. A bunch of other posts came up and had some code, but none of it did what I wanted (including converting screen space coordinates back into world space coordinates of the UI object which was stupid easy and not reversibe, thanks RectTransformUtility!)

Categories