I'm trying to make a camera track two targets in a 2D game on unity but I can't quite get it to work.
This is the code I currently have, but it's changing the rotation in transform instead of position. the ortho size is changing but it is not properly tracking the center point between the characters. is there anyway I can fix this?
using System.Collections.Generic;
using UnityEngine;
public class CameraZoom : MonoBehaviour
{
private Camera cameraRef;
private GameObject[] playerPos;
void Start()
{
cameraRef = GetComponent<Camera>();
playerPos = GameObject.FindGameObjectsWithTag("Player");
Debug.Log(playerPos[0].transform.position);
Debug.Log(playerPos[1].transform.position);
Debug.Log(cameraRef.tag);
StartCoroutine(ZoomInOut());
}
// Update is called once per frame
void Update()
{
}
IEnumerator ZoomInOut()
{
while (true)
{
if (playerPos[0] != null && playerPos[1] != null)
{
Vector3 lookPoint = Vector3.Lerp(playerPos[0].transform.position, playerPos[1].transform.position, 0.5f);
cameraRef.transform.LookAt(lookPoint);
float distance = Vector3.Distance(playerPos[0].transform.position, playerPos[1].transform.position);
if (distance > (cameraRef.orthographicSize * 2))
{
cameraRef.orthographicSize += 0.05f;
// if (distance < (cameraRef.orthographicSize * 2))
//{
// cameraRef.orthographicSize -= 0.1f;
//}
}
else
if (distance < (cameraRef.orthographicSize))
{
cameraRef.orthographicSize -= 0.05f;
}
yield return new WaitForSeconds(0.02f);
}
}
}
}
Well cameraRef.transform.LookAt does exactly what you described
Rotates the transform so the forward vector points at worldPosition.
Since your game is 2D anyway what you rather want to do is set the camera to exactly the same XY position
var lookPoint = (playerPos[0].transform.position, playerPos[1].transform.position) * 0.5f;
//Maintain the Z depth position
lookPoint.z = cameraRef.transform.position.z;
cameraRef.transform.position = lookPoint;
Also instead of
yield return new WaitForSeconds(0.02f);
rather use
yield return null;
The latter makes it run every frame. But anyway assuming 60FPS a frame takes about 0.017seconds so waiting0.02` will most probably mean you wait two frames since this value is not the exact time but rather the minimum time to wait/skip to the next frame.
And move this OUT of the if block!!
If one of your objects is missing => endless loop -> Freeze/Crash of Unity and your app! You definitely want to wait one frame for every iteration of your while loop!
Actually you could as well do the thing in Update without the loop at all :)
And then instead of
cameraRef.orthographicSize -= 0.05f;
I would rather either use
// Take differences in the frame rate into account (jitter)
cameraRef.orthographicSize -= 3 * Time.deltaTime;
or directly interpolate
// Smooth interpolate -> if the target and current value are far apart
// this zooms faster and then becomes slower when already very close
// Adjust that "5" according to your needs
cameraRef.orthographicSize = Mathf.Lerp(cameraRef.orthographicSize, distance, 5 * Time.deltaTime);
without using your conditions at all.
Typed on the phone but I hope the idea gets clear
Related
Here is a simple script to demonstrate the issue. This script should move the cursor to the left for 1 second and then back to the right for 1 second. However you'll see it just moves it to the left then stops. You can flip the order and try to move right first, but you'll notice that still won't work. I've also tested with moving up / down, moving up works as expected, moving down does not.
public class dumbtestscript : MonoBehaviour
{
void Start()
{
Mouse.current.WarpCursorPosition(new Vector3(123, 123));
StartCoroutine(MoveCursorLeftThenRightCoroutine());
}
IEnumerator MoveCursorLeftThenRightCoroutine()
{
float startTime = Time.time;
while (Time.time < startTime + 1)
{
Mouse.current.WarpCursorPosition(Input.mousePosition +
(Time.deltaTime * new Vector3(-0.1f, 0)));
yield return null;
}
while (Time.time < startTime + 2)
{
Mouse.current.WarpCursorPosition(Input.mousePosition +
(Time.deltaTime * new Vector3(0.1f, 0)));
yield return null;
}
}
}
Am I misunderstanding something about how WarpCursorPosition is supposed to work?
Any advice appreciated. Thanks.
Edit: Upon further inspection it may have to do with the vector being passed to WarpCursorPosition. For example regardless of whether we use this to move left:
Mouse.current.WarpCursorPosition(Input.mousePosition + new Vector3(-0.1f, 0));
or this:
Mouse.current.WarpCursorPosition(Input.mousePosition + new Vector3(-1f, 0));
it moves to the left at the same speed. So it seems anything between -0.1 and -1 is being rounded to -1. Conversely for going right everything between 0.1 and just under 1 is being rounded to 0 which would explain why it wasn't moving in the original.
So everything is getting round down to the nearest integer for some reason? Both Input.mousePosition and the Vector3 we are adding are both Vector3 and I thought could handle float numbers so not sure why things are being rounded down to ints, if thats whats happening?
The issue
So everything is getting round down to the nearest integer for some reason?
Most probably Yes!
WarpCursorPosition makes your actual system cursor jump to the given pixel coordinates.
While inside Unity the pixel space is always provided as float for many things and mainly for making direct calculations with it, the actual system cursor uses pixels (like everything that is actually happening on the screen) which are always full int values!
So, yes, it will always (not round but rather) floor your given input to the next int (full pixel). Basically what happens if you simply use
var x = (int) (Input.mousePosition.x - 0.1f);
=> You will see that x will be exactly Input.mousePosition.x - 1 since if e.g. the current Input.mousePosition.x was 230 then you get (int) 229.9f which is 229.
In the other direction though you get e.g. 230 + 0.1f so (int) 230.1f again simply is 230.
=> Your steps would need to at least be one full pixel!
Solution
So instead of constantly read the current Input.mousePosition which underlies the afore mentioned pixel coordinates rather use and update a local vector that does not:
IEnumerator MoveCursorLeftThenRightCoroutine()
{
// Store the current mousePosition as a Vector2
// This uses floats and is not restricted to full pixel jumps
var currentPosition = Input.mousePosition;
for(var passedTime = 0f; passedTime < 1; passedTime += Time.deltaTime)
{
// simply continously add to the float vector
currentPosition += Vector2.left * speedInPixelsPerSecond * Time.deltaTime;
// Using that float vector as input may in some frames cause no movement where the clamp results
// still in the same pixel but it will catch up as soon as the vector was increased/reduced enough
Mouse.current.WarpCursorPosition(currentPosition);
yield return nnull;
}
for(var passedTime = 0f; passedTime < 1; passedTime += Time.deltaTime)
{
currentPosition += Vector2.right * speedInPixelsPerSecond * Time.deltaTime;
Mouse.current.WarpCursorPosition(currentPosition);
yield return null;
}
}
In general for the new Input system I would rather use Mouse.current.position instead of Input.mousePosition.
I don't have any experience using WarpCursorPosition but when I was experimenting to answer this thread, I felt it's not consistent & would like to advice not to use it.
Code
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
public class Q69189325 : MonoBehaviour
{
private void Start()
{
Mouse.current.WarpCursorPosition(Input.mousePosition);
StartCoroutine(MoveLogicCrt());
}
IEnumerator MoveLogicCrt()
{
yield return MoveCursorToDirectionCrt(Vector3.left, 150, 5);
yield return null;
yield return MoveCursorToDirectionCrt(Vector3.right, 350, 5);
}
private IEnumerator MoveCursorToDirectionCrt(Vector3 direction, float speed, float movementTime)
{
float startTime = Time.time;
while(Time.time < startTime + movementTime)
{
Vector2 newMousePosition = Input.mousePosition + (Time.deltaTime * direction * speed);
Mouse.current.WarpCursorPosition(newMousePosition);
yield return null;
}
}
}
Note:
For some reason when I put the same speed to both left & right it doesn't move right which was weird.
Execution Overview
https://gfycat.com/eventhriftyannelida
Happy coding!
I used from this code but it does not do it:
public Transform[] points;
public float speed;
private void OnTriggerEnter2D(Collider2D other)
{
if (other.tag == "door1")
{
transform.position = Vector3.Lerp(transform.position, points[1].position, speed * Time.deltaTime);
}
}
That is, I want the pig to go to a higher point on the ground at a desired speed when it hits the trigger (see the photo attached to see it)
Two problems here:
you are calling that line exactly once when you enter the collider so the movement is applied for I single frame!
Lerp interpolates linear between two positions using a factor between 0 and 1. You every time use the current position as start point so what would happen if if you called this continuously is approximating the position getting slower and slower every frame which is not what you describe. You want to move with a constant speed.
You most likely would rather use a Coroutine and MoveTowards for that
private void OnTriggerEnter2D(Collider2D other)
{
// Better use CompareTag here
if (other.CompareTag("door1"))
{
// Start a routine for the continuous movement
StartCoroutine(MoveTo(points[1].position, speed);
}
}
private IEnumerator MoveTo(Vector3 targetPosition, float linearSpeed)
{
// This uses an approximation of 0.00001 for equality
while(transform.position != targetPosition)
{
// with a constant speed of linearSpeed Units / second
// move towards the target position without overshooting
transform.position = Vector3.MoveTowards(transform.position, targetPosition, linearSpeed * Time.deltaTime);
// Tell Unity to "pause" the routine here, render this frame
// and continue from here in the next frame
yield return null;
}
// to be sure to end up with exact values set the target position fix when done
transform.position = targetPosition;
}
Alternatively a bit more complex looking but more powerful would be to rather calculate the required time depending on the speed but still adding some smoothing like e.g.
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("door1"))
{
StartCoroutine (MoveTo(points[1].position, speed);
}
}
private IEnumerator MoveTo(Vector3 targetPosition, float averageSpeed)
{
// store the initial position
var from = transform.position;
// Get the expected duration depending on distance and speed
var duration = Vector3.Distance(from, targetPosition) / averageSpeed;
// This is increased over time
var timePassed = 0;
while(timePassed < duration)
{
// This linear grows from 0 to 1
var factor = timePassed / duration;
// Adds some ease-in and ease-out at beginning and end of the movement
factor = Mathf.SmoothStep(0, 1, factor);
// linear interpolate on the smoothed factor
transform.position = Vector3.Lerp(from, targetPosition, factor);
// increase by time passed since last frame
timePassed += Time.deltaTime;
// Tell Unity to "pause" the routine here, render this frame
// and continue from here in the next frame
yield return null;
}
// to be sure to end up with exact values set the target position fix when done
transform.position = targetPosition;
}
OnTriggerEnter2D is only called once on every collider enter. Given you are only moving it by speed * Time.deltaTime with Time.deltaTime being in the order of 0.008 - 0.100 it may only move slightly.
Depending on what you want, are you sure you don't want to completely move the object or alternatively set a flag that starts moving it in the update() method?
I'm trying to create a smooth camera movement in 2D. The target I want to hit can potentially move a large distance in a single frame, and is not like a character moving smoothly from A to B.
I'm aware of possible solutions like using Vector2.Lerp(), but that approach only slows down nicely but speeds up abruptly.
_position = Vector2.Lerp(_position, target, 0.5f * Time.deltaTime);
I've tried implementing the "arrive" steering behaviour, but cannot make it work nicely together with acceleration - especially when the target is close to the current position.
I managed to make it work pretty well in one axis, but that approach didn't work when repeated in a second axis.
var decelerateRadius = GetDistanceFromAcceleration(acceleration, Mathf.Abs(_velocity));
var direction = target - _position;
var distance = Mathf.Abs(direction);
var a = acceleration * Time.deltaTime;
if (distance > 0.0005f)
{
if (distance < decelerateRadius.x)
{
_velocity *= distance / decelerateRadius.x;
}
else
{
_velocity += direction.x >= 0.0f ? a : -a;
}
}
else
{
_velocity = 0.0f;
}
// move tracker
_position += _velocity * Time.deltaTime;
And my method for calculating the distance based on acceleration:
private Vector2 GetDistanceFromAcceleration(float a, float vx, float vy)
{
// derived from: a = (vf^2 - vi^2) / (2 * d)
return new Vector2(vx * vx / (2.0f * a), vy * vy / (2.0f * a));
}
My last attempt was making a rolling average of the target, but it suffered the same issue as lerping.
To summarize the requirements:
Must accelerate
Must decelerate and stop at target
Must not "orbit" or in other ways swing around the target, before stopping
Target must be able to move
May be limited by a maximum velocity
Any tips, pointers og solutions on how to achieve this?
I've also asked the question over at game dev
https://gamedev.stackexchange.com/questions/170056/accelerate-decelerate-towards-moving-target-and-hitting-it
The problem with your lerp is also that you actually never reach the target position you just get very very close and small.
I thought about something like this
as long as you are already at the targets position don't move. Enable orbit mode
while not within a certain targetRadius around the target position accelerate from to maxVelocity
if getting within a certain targetRadius around the target position decelerate depending on the distance / radius will be a value between 1 and 0
To get the distance there is already Vector2.Distance you could/should use.
For the movement I would recommend Vector2.MoveTowards which also avoids overshooting of the target.
something like
public class SmoothFollow2D : MonoBehaviour
{
[Header("Components")]
[Tooltip("The target this will follow")]
[SerializeField] private Transform target;
[Header("Settings")]
[Tooltip("This may never be 0!")]
[SerializeField] private float minVelocity = 0.1f;
[SerializeField] private float maxVelocity = 5.0f;
[Tooltip("The deceleration radius around the target.\nThis may never be 0!")]
[SerializeField] private float targetRadius = 1.0f;
[Tooltip("How much speed shall be added per second?\n" +
"If this is equal to MaxVelocity you know that it will take 1 second to accelerate from 0 to MaxVelocity.\n" +
"Should not be 0")]
[SerializeField] private float accelerationFactor = 3.0f;
private float _currentVelocity;
private float _lastVelocityOutsideTargetRadius;
private bool _enableOrbit;
public bool EnableOrbit
{
get { return _enableOrbit; }
private set
{
// if already the same value do nothing
if (_enableOrbit == value) return;
_enableOrbit = value;
// Whatever shall be done if orbit mode is enabled or disabled
}
}
private void Update()
{
if (target == null) return;
var distanceToTarget = Vector2.Distance(transform.position, target.position);
// This is the threshold Unity uses for equality of vectors (==)
// you might want to change it to a bigger value in order to
// make the Camera more stable e.g.
if (distanceToTarget <= 0.00001f)
{
EnableOrbit = true;
// do nothing else
return;
}
EnableOrbit = false;
if (distanceToTarget <= targetRadius)
{
// decelerate
// This will make it slower
// the closer we get to the target position
_currentVelocity = _lastVelocityOutsideTargetRadius * (distanceToTarget / targetRadius);
// as long as it is not in the final position
// it should always keep a minimum speed
_currentVelocity = Mathf.Max(_currentVelocity, minVelocity);
}
else
{
// accelerate
_currentVelocity += accelerationFactor * Time.deltaTime;
// Limit to MaxVelocity
_currentVelocity = Mathf.Min(_currentVelocity, maxVelocity);
_lastVelocityOutsideTargetRadius = _currentVelocity;
}
transform.position = Vector2.MoveTowards(transform.position, target.position, _currentVelocity * Time.deltaTime);
}
// Just for visualizing the decelerate radius around the target
private void OnDrawGizmos()
{
if (target) Gizmos.DrawWireSphere(target.position, targetRadius);
}
}
The MinVelocity is actually necessary for the edge case when the target is moved not further than TargetRadius and lastVelocityOutsideTargetRadius si still 0. In that case no acceleration takes place so lastVelocityOutsideTargetRadius is never updated.
With the values you have to play a bit ofcourse ;)
It might not be perfect yet but I hope it is a good start point to develop that further (only looks laggy due to 15 FPS for the Gif ;) )
this may be a dumb question...
I have two variables and I am setting them to serial data that continuosly changes (from an arduino), just at different times. Then, I am comparing them with each other to see if an object (The IKTarget) should move forwards or backwards(in unity).
Below is my code:
void Start() {
//Starting a new coroutine
StartCoroutine(VariableSetter());
}
//Using that corouting
IEnumerator VariableSetter() {
//Setting the first variable to the serialdata
controllerfinger1_Angle = SerialDataDecoder.finger1_Angle;
//Waiting 20ms
yield return new WaitForSeconds(0.02f);
//Setting the other variable to the serialdata
newcontrollerfinger1_Angle = SerialDataDecoder.finger1_Angle;
}
void Update() {
//Making the finger movement frame dependent
amountToMove = speed * Time.deltaTime; */
//If the finger angle is increasing
if (newcontrollerfinger1_Angle > controllerfinger1_Angle)
{
//Move the IKTarget fowards (Closes the finger)
IKTarget.Translate(Vector3.forward * amountToMove, Space.World);
}
//If the finger angle is decreasing
if (newcontrollerfinger1_Angle < controllerfinger1_Angle)
{
//Move the IKTarget backwards (Opens up the finger)
IKTarget.Translate(Vector3.back * amountToMove, Space.World);
}
}
When I run this in unity, the IKTarget object doesnt seem to want to move.
I KNOW my SerialData is correct as I have checked.
If anybody could provide an answer for this issue, it would be greatly appreciated.
Thanks,
Isaac.
while (transform.position != new Vector3(desX, desY))
{
// 2 - Movement
Vector3 movement = new Vector3(
0.1f * desX,
0.1f * desY,
0);
//movement *= Time.deltaTime;
transform.Translate(movement);
}
This part of my program crashes the Unity engine and I'm pretty sure it's an infinite loop but I can't figure out why or how to fix it.
It's freezing your application because you're are not giving other scripts chance to run when the condition in the while loop is not met.
To fix that put that code in a coroutine function then add yield return null; to the while loop. This makes Unity to wait for a frame after each loop therefore given other scripts the opportunity to run every frame. This should fix the freeze issue whether the while loop exits or not. I would also suggest you use Vector3.Distance to determine when you are close to the destination.
public float reachThreshold = 0.2f;
void Start()
{
StartCoroutine(MoveBject());
}
IEnumerator MoveBject()
{
float distance = Vector3.Distance(transform.position, new Vector3(desX, desY));
while (distance > reachThreshold)
{
// 2 - Movement
Vector3 movement = new Vector3(
0.1f * desX,
0.1f * desY,
0);
//movement *= Time.deltaTime;
transform.Translate(movement);
//Wait a frame
yield return null;
}
}
If you really want to move GameObject to another position over time, see this post.
In general don't do things in while that are meant to happen in a per frame base! (Thanks Ron)
The result of while in the best case would be that your App stucks until eventually the vectors match, making the object "jump" in position. In the worst case they never match and your App freezes forever.
Instead you should use the Update() method, which is called each frame, and just move the object one step per frame.
To compare the Vectors you should better use Vector3.Distance. Using the == operator for Vector3 is basically ok but internally it does something equal to
Vector3.Distance(vectorA, vectorB) <= 0.00001f
which uses a very small thershold that might not match exactly. So Using Vector3.Distance you can set your own threshold e.g. to 0.1 to make it "easier" to match.
bool match = Vector3.Distance(transform.position, new Vector3(desX, desY) < 0.1f
To move an object towards another Unity actually already has a built-in method Vector3.MoveTowards e.g.
transform.position = Vector3.MoveTowards(transform.position, new Vector3 (desX, desY), 0.1f);
This takes care of Vectors3 comparing so you don't even need it anymore.
To make it smooth and framarate-save you were right already to use Time.deltaTime. Without this it would move 0.1 meters / frame but your framerate might not be stabil. Using Time.deltaTime makes it 0.1 meters / second which in almost all cases is what you actually want to achieve.
So to put it together your code should be something like
float desX;
float desY;
float moveSpeed = 0.1f;
void Update()
{
var targetPosition = new Vector3 (desX, desY);
transform.position = Vector3.MoveTowards(transform.position, targetPosition, moveSpeed * Time.deltaTime);
}
Note that in some cases MoveTowards still is not relayable enough e.g. if you want to track collisions or something. In this case refer here