[SOLVED] Help creating a dynamically moving camera

Currently, I am trying to make my game camera smoothly move from 3rd person view to top down view such as this poorly drawn image.
3105700--234628--Untitled.png
I am having issues getting the camera to end in the proper rotation and position. Due to my lackluster mathematics skills and lack of knowledge in any form of animation, my script might seem a bit messy as I have been trying everything that came to mind.

Currently, the camera makes it almost to the desired location/rotation which I think it caused do to a lack of a better way of telling the camera when it hit its destination.

As a side note, before questions are asked, I am using Time.realtimeSinceStartup because the game is supposed to pause when there is not an input.

This is my script(try not to cringe):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraAnimation : MonoBehaviour {

    #region variables
    public float topdownHeight = 10;
    public float speed;

    private float startTime;
    private float timer = 0f;

    private bool isRunning = false;

    Vector3 endPoint;

    private Vector3 distance;

    public enum FocusState
    {
        ABILITYTOPDOWN,
        SELECTEDPLAYER,
        OFF
    }
    [HideInInspector]
    public FocusState focusState = FocusState.OFF;

    [HideInInspector]
    public GameObject selectedPlayer;
    #endregion variables

    void Update () {

        switch (focusState)
        {
            case (FocusState.ABILITYTOPDOWN):
                timer = Time.realtimeSinceStartup;
                TopDown();
                break;
            case (FocusState.SELECTEDPLAYER):

                break;
            case (FocusState.OFF):
                break;
        }
    }

    private void TopDown()
    {
        if (!isRunning)
        {
            Debug.Log("Running...");
           
            startTime = Time.realtimeSinceStartup;
            Debug.Log(startTime);

            Debug.Log("Start Pos: " + transform.position);
            endPoint = new Vector3(transform.position.x, transform.position.y + topdownHeight, transform.position.z);
            Debug.Log("End Pos: " + endPoint);

            isRunning = true;

            distance = endPoint - selectedPlayer.transform.position/speed;

        }

        if(Mathf.Abs(transform.position.y) >= Mathf.Abs(endPoint.y) && Mathf.Abs(transform.position.x) >=
            Mathf.Abs(endPoint.x) && Mathf.Abs(transform.position.z) >= Mathf.Abs(endPoint.z) && gameObject.transform.localRotation.x == 90)
            //Vector3.Distance(transform.position, endPoint) <= 1)
        {
            focusState = FocusState.OFF;
            timer = 0;
            isRunning = false;
        }
        else
        {
            Debug.Log(timer);
            gameObject.transform.position = new Vector3(transform.position.x + (distance.x*(timer-startTime)),
                transform.position.y + (distance.y * (timer - startTime)), transform.position.z+(distance.z * (timer - startTime)));
            Debug.Log("moved camera up by " + (Time.deltaTime - startTime) * speed);
            transform.LookAt(selectedPlayer.transform.position);
        }
    }
}

If the end position is just above the object (character?) I might try using Vector3.Slerp (Unity - Scripting API: Vector3.Slerp) inside a coroutine.
Oh, that’s just for its position, btw. You can try setting LookAt as it goes, or at the end and see if that’s good…

Checking if position coordinates are >< than some value in world space is a recipe for disaster, what if your character turns around or something?

IMO best to use two empty gameobjects as targets, then as methos5k said, lerp between them for the two different views.

I have tried following the slerp reference page but I still get the same issues of the camera not ending in the correct positon. As far as the rotation, that should fix itself once the position is figured out. I am also trying to move the code into coroutines to make it easier to link up with the UI buttons and not have to use the Update method, but rather than the desired effect, I have successfully found a new way to make unity instantly crash. I am sure the crashing has something to do with some stupid mistake in the coroutine.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraAnimation : MonoBehaviour {

    #region variables
    public Transform topDown;
    public Transform thirdPerson;
    private Transform camTransform;
    public float speed;
    private float startTime;

    private bool isRunning = false;

    public enum FocusState
    {
        ABILITYTOPDOWN,
        SELECTEDPLAYER,
    }
    [HideInInspector]
    public FocusState focusState = FocusState.SELECTEDPLAYER;

    [HideInInspector]
    public GameObject selectedPlayer;
    #endregion variables

    void Update () {
       
    }

    public void AbilityButton()
    {
        if(focusState == FocusState.SELECTEDPLAYER && isRunning == false)
        {
            camTransform = transform;
            StartCoroutine(TransitionTopDown());
        }
        else
        {
            //
        }
    }

    private IEnumerator TransitionTopDown()
    {
        if (!isRunning)
        {
            Debug.Log("Running...");
            startTime = Time.realtimeSinceStartup;
            Debug.Log(startTime);
            isRunning = true;
        }

        if (transform.position == topDown.position)
        {
            Debug.Log("Camera movement complete");
            focusState = FocusState.ABILITYTOPDOWN;
            isRunning = false;
            StopCoroutine(TransitionTopDown());
        }
        Vector3 center = (camTransform.position + topDown.position);// * 0.5F;
        //center -= new Vector3(0, 1, 0);
        Vector3 startRelCenter = camTransform.position - center;
        Vector3 endRelCenter = topDown.position - center;
        float fracComplete = (Time.realtimeSinceStartup - startTime) / speed;
        transform.position = Vector3.Slerp(startRelCenter, endRelCenter, fracComplete);
        transform.position += center;
        transform.LookAt(selectedPlayer.transform.position);

        yield return TransitionTopDown();
    }
}

You were close, I think this is what you want. The main differences are the center and using thirdPerson, not camTransform, for the startRelCenter vectors.

With the code you had the camera should have been in the correct position once the coroutine was complete, even if the path/movement it was taking wasn’t what you wanted. Are you sure your topDown transform is where you think it is? Is it a child of the selectedPlayer? A fixed position in the scene?

// Center is the center of the "sphere" that Slerp will interpolate around
// We'll use the yposition of the thirdPerson transform, that should produce a natural looking curve
Vector3 center = topDown.position;
center.y = thirdPerson.position.y;

Vector3 startRelCenter = thirdPerson.position - center;
Vector3 endRelCenter = topDown.position - center;

float fracComplete = (Time.realtimeSinceStartup - startTime) / speed;
transform.position = Vector3.Slerp(startRelCenter, endRelCenter, fracComplete);
transform.position += center;

transform.LookAt(selectedPlayer.transform.position);

This is how my camera is currently set up in my scene. The camera is not a child of the selected character but is a child of a container that moves to the position of a character when they are selected.
3106601--234718--Inspector.PNG
Main camera is the camera and the other two are empty game objects holding the transform positions the camera moves to as suggested by Billy4184.

Edit* I cannot tell if the provided code segment works because I am still having an issue with unity crashing.

Edit** The code does work but I had to go back to the original setup with the Update method

You can do something like this:

IEnumerator TransitionTopDown() {
Vector3 startPos = startpos;
Vector3 endPos = endpos;
float timeToMove = whateverTime;
for(float t = 0; t <= timeToMove; t+=Time.deltaTime) {
  transform.position = Vector3.Slerp(startPos, endPos, t / timeToMove);
  yield return null;
 }

I didn’t include your pausing code, but you’ll notice this coroutine yields a frame and accumulates time, as part of the Slerp Method parameter. It uses the starting position (from the very beginning of the call) the whole time…and doesn’t try to call the coroutine, again, as your earlier example did.
I think that’ll work for you if you try this route. You could add the pause scenarios, and also send the start and end variables as parameters, if you want this method to run both ways, too :slight_smile:

First, why are you calling ‘yield return TransitionTopDown()’ instead of just ‘yield return null’? I’ve never used a coroutine like that but it seems like it would call itself recursively and crash.

You can just put the code inside a while loop and use ‘yield return null’ instead:

while ((Time.realtimeSinceStartup - startTime)/speed < 1)
{

    // Some code


    yield return null;
}

Also when you are trying to move stuff precisely around a dynamic object, it’s usually best to work in local coordinates to avoid ‘catching up’ problems. The two positions are already children of the container, and if you use the container as the center then, since it’s local position to itself is Vector3.zero, you don’t have to worry about adjusting it.

Also, since you are adjusting the slerp value, the start and end positions must not change, you cannot use the world position or the current position of the transform, rather you must store the start and end positions at the beginning.

private IEnumerator TransitionTopDown()
{

    float startTime = time.realtimeSinceStartup;
    Vector3 startPos = transform.localPosition;
    Vector3 endPos = topDown.localPosition;

 
    while ((Time.realtimeSinceStartup - startTime)/speed < 1)
    {
        float frac = (Time.realtimeSinceStartup - startTime)/speed;
        transform.localPosition = Vector3.Slerp(startPos, endPos, frac);
        transform.LookAt(selectedPlayer.transform.position);

        yield return null;
    }
}

Thank you Billy4184, your solution worked perfectly.

I have modified the solution to work with my script incase anyone is interested.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraAnimation : MonoBehaviour {

    #region variables
    public Transform topDown;
    public Transform thirdPerson;

    private bool isRunning = false;

    public enum CameraPosition
    {
        THIRDPERSON,
        ABILITYTOPDOWN
    }

    [HideInInspector]
    public CameraPosition cameraPosition = CameraPosition.THIRDPERSON;

    [HideInInspector]
    public GameObject selectedPlayer;
    #endregion variables

    public void AbilityButton()
    {

        if(cameraPosition == CameraPosition.THIRDPERSON && isRunning == false)
        {
            StartCoroutine(TransitionCamera(transform, topDown, 0.75f));
            cameraPosition = CameraPosition.ABILITYTOPDOWN;
        }
        else if(cameraPosition == CameraPosition.ABILITYTOPDOWN && isRunning == false)
        {
            StartCoroutine(TransitionCamera(transform, thirdPerson, 0.75f));
            cameraPosition = CameraPosition.THIRDPERSON;
        }

    }

    IEnumerator TransitionCamera(Transform start, Transform end, float speed)
    {
        isRunning = true;
        float startTime = Time.realtimeSinceStartup;
        Vector3 startPos = start.localPosition;
        Vector3 endPos = end.localPosition;

        while ((Time.realtimeSinceStartup - startTime) / speed < 1)
        {
            float frac = (Time.realtimeSinceStartup - startTime) / speed;
            transform.localPosition = Vector3.Slerp(startPos, endPos, frac);
            transform.LookAt(selectedPlayer.transform.position);

            yield return null;
        }
        isRunning = false;
    }
}