3
\$\begingroup\$

I am using Unity3D, and I am building an isometric world by rotating a plane on the Y axis by 4.205 units.

I am using an orthographic camera. I want to constrain the camera so that the viewport never shows the area outside of the world (in green).

Isometric map

  1. How can I calculate the corner points of my rotated plane in screen coordinates, represented by green?
  2. How do I stop the camera from scrolling to prevent it from showing the area outside the world, represented by gray?
\$\endgroup\$
  • \$\begingroup\$ Yes, but how would I detect the gray area is shown? It's simply the infinite area outside the plane that Unity supplies. \$\endgroup\$ – Simian May 2 '17 at 17:36
  • \$\begingroup\$ This does depend on the kind of game you're making but if it's anything like a strategy and especially if you're allowing zoom-in and zoom-out, this type of camera will just be annoying. I suggest restricting the middle point of the camera to the plane instead. \$\endgroup\$ – John Hamilton May 5 '17 at 7:57
  • 1
    \$\begingroup\$ Agreed, this is good advice. However, my type of game will not allow zooming in or out on the map. \$\endgroup\$ – Simian May 8 '17 at 3:26
  • \$\begingroup\$ Where is your camera looking towards? Is it a bit tilted or looking directly towards Z+? \$\endgroup\$ – John Hamilton May 8 '17 at 5:01
2
+25
\$\begingroup\$

Restrict only camera viewport centre to ground plane

You can find the working Unity project containing this code here.

using UnityEngine;
using System.Collections;

public class CameraAnchorController : MonoBehaviour
{
    public float xAcceleration = 0;
    public float yAcceleration = 0;
    public float accelerationRate; //set to 1 in inspector
    public Vector2 acceleration;
    public Vector2 velocity;

    float friction = 0.98f;
    public float xMin; //set to -5 in inspector
    public float xMax; //set to 5 in inspector
    public float yMin; //set to -5 in inspector
    public float yMax; //set to 5 in inspector
    //these -5/+d5 are because a Unity plane's origin is in the centre.

    void Update()
    {
        xAcceleration = 0;
        yAcceleration = 0;

        //acceleration
        xAcceleration += Input.GetKey(KeyCode.LeftArrow)    ? -1 : 0;
        xAcceleration += Input.GetKey(KeyCode.RightArrow)   ? +1 : 0;
        yAcceleration += Input.GetKey(KeyCode.UpArrow)      ? +1 : 0;
        yAcceleration += Input.GetKey(KeyCode.DownArrow)    ? -1 : 0;
        acceleration = new Vector2(xAcceleration, yAcceleration); //use Vector2 here or we incur extra, costly sqrt calls in magnitude.
        acceleration = Vector2.ClampMagnitude(acceleration, 1.0f); //normalize it.
        acceleration *= accelerationRate;

        //speed
        velocity += acceleration;
        velocity *= friction;

        //position
        float xPos = transform.localPosition.x + velocity.x * Time.deltaTime;
        float yPos = transform.localPosition.z + velocity.y * Time.deltaTime; //note the interchanged y/z here due to Unity's coord system.

        //We'd usually just use 2 Mathf.Clamp(val, min, max) calls here, but we need to know
        //the outcome of the clamping, so as to also restrict velocity if we hit a map side.
        if (xPos < xMin)
        {
            xPos = xMin; 
            velocity = new Vector2(0, velocity.y); 
        }
        if (xPos > xMax)
        {
            xPos = xMax; 
            velocity = new Vector2(0, velocity.y);
        }
        if (yPos < yMin)
        {
            yPos = yMin; 
            velocity = new Vector2(velocity.x, 0);
        }
        if (yPos > yMax)
        {
            yPos = yMax; 
            velocity = new Vector2(velocity.x, 0);
        }

        xPos = Mathf.Clamp(xPos, xMin, xMax); //limit to the plane's x     extent in local space
        yPos = Mathf.Clamp(yPos, yMin, yMax); //limit to the plane's y (z) extent in local space

        //update transform
        transform.localPosition = new Vector3(xPos, 0, yPos);
    }
}

In Unity: Create a plane, create a sphere, then drag the sphere onto the plane so the plane is its parent. Now the sphere is moving in the plane's local space. You can name it "Camera anchor". Drag the Camera onto the anchor so that the anchor is its parent. Set your camera's x rotation to the required tilt in the inspector and its y rotation to 45; zero its position and then drag it out along local z axis to required viewing distance to see ground and anchor. Drag this script onto the anchor. Create a directional light. Using the arrow keys, you will see how the anchor restricts the camera's look vector (in centre of its viewport) so that it never leaves the ground plane. Notice that your controls are a bit funny, and treat up/down and left/right as diagonal planes. Fix is to replace:

    xAcceleration += Input.GetKey(KeyCode.LeftArrow)    ? -1 : 0;
    xAcceleration += Input.GetKey(KeyCode.RightArrow)   ? +1 : 0;
    yAcceleration += Input.GetKey(KeyCode.UpArrow)      ? +1 : 0;
    yAcceleration += Input.GetKey(KeyCode.DownArrow)    ? -1 : 0;

with

    if (Input.GetKey(KeyCode.UpArrow))
    {
        xAcceleration += +1;
        yAcceleration += +1;
    }
    if (Input.GetKey(KeyCode.DownArrow))
    {
        xAcceleration += -1;
        yAcceleration += -1;
    }
    if (Input.GetKey(KeyCode.LeftArrow))
    {
        xAcceleration += -1;
        yAcceleration += +1;
    }
    if (Input.GetKey(KeyCode.RightArrow))
    {
        xAcceleration += +1;
        yAcceleration += -1;
    }

Note that you can effectively zoom in and out by changing ortho camera's size in the inspector.

This eliminates any need to perform matrix transforms, and solves other problems (below).

Restrict to camera edges to near ground plane edges

Let's say things are exactly as in your diagram - tiny world relative to large camera viewport. In that case, I can see exactly how you expect this to work. We'd pan the red rectangle around in a screen-axis aligned rectangle that is a bit larger, and exactly contains the isometric ground plane. We'll call this Implementation Style A. This is what Bálint has very reasonably suggested, given your diagram. But what if you have a large map with smaller viewport? I doubt then that this will be the ideal solution for you: even with a map just 4x the size of that in your diagram, you're going to be able to scroll into large, dark spaces in the top left, top right, bottom left and bottom right of the map, because you're only restricting by a screen-space rectangle. Not good?

I imagine you'd then want a different mechanism, one which locks the camera so its centre can't leave the bounds of the ground plane; you could then never run the camera into large, void spaces. I've supplied this above, call it Implementation Style B.

In either implementation, you'll need sufficient dummy tiles on the outside of the ground plane to ensure that at maximum zoom, you will never see black space. I've seen this done in some of the XCom / UFO games. Calculating how many rows / columns of extra tiles you would need exactly, is going to depend on the size value on your camera (which links directly to viewport height in unity), and some calculation of your tile width as against your tile height, which can be accomplished with Mathf.Sin(). You may also need a size value that relates to horizontal vs vertical - that is as simple as size * Screen.width / Screen.height. Still, all of this is probably unnecessary as you can manually test how many tiles worth of buffering works for you. Test against a 2:1 screen ratio to be very sure you've got it covered.

For the above solution, to create a buffer space inside your existing tile map, replace this:

    //position
    float xPos = transform.localPosition.x + velocity.x * Time.deltaTime;
    float yPos = transform.localPosition.z + velocity.y * Time.deltaTime; //note the interchanged y/z here due to Unity's coord system.

with:

       //position
        float xPos = transform.localPosition.x + velocity.x * Time.deltaTime;
        float yPos = transform.localPosition.z + velocity.y * Time.deltaTime; //note the interchanged y/z here due to Unity's coord system.

        float buffer = 2f; //2 units in world space which might be 2 tiles across in your code.
        float xMinLimited = xMin + buffer;
        float xMaxLimited = xMax - buffer;
        float yMinLimited = yMin + buffer;
        float yMaxLimited = yMax - buffer;

        //We'd usually just use 2 Mathf.Clamp(val, min, max) calls here, but we need to know
        //the outcome of the clamping, so as to also restrict velocity if we hit a map side.
        if (xPos < xMinLimited)
        {
            xPos = xMinLimited; 
            velocity = new Vector2(0, velocity.y); 
        }
        if (xPos > xMaxLimited)
        {
            xPos = xMaxLimited; 
            velocity = new Vector2(0, velocity.y);
        }
        if (yPos < yMinLimited)
        {
            yPos = yMinLimited; 
            velocity = new Vector2(velocity.x, 0);
        }
        if (yPos > yMaxLimited)
        {
            yPos = yMaxLimited; 
            velocity = new Vector2(velocity.x, 0);
        }
\$\endgroup\$
  • \$\begingroup\$ P.S. I realised it is possible to adapt this to the "moving inside a screen space rect" approach by rotating the ground plane around the y axis by 45 degrees, thus creating a screen-aligned rect in which to move. And then using a separate plane that fits inside it, in isometric orienation. Some of the arithmetic will need to change but it's pretty trivial. Let me know if you're interested, @Simian. \$\endgroup\$ – Engineer May 9 '17 at 17:43
0
\$\begingroup\$

An isometric coordinate system is basically just a rotated, and scaled coordinate system.

First you need to create a rotation matrix with an angle of -45 degrees if your forward is to the top-left and 45 degrees if it's to the top-right.

Then you create a scaling matrix with a scale vec3(1, d, 1) where đ depends on the angle of the isometric view. Take the height of the tile and divide it with the width.

You can't use Matrix4.TRS for this, you have to multiply the scale matrix with the rotation matrix first:

screenPos = rot * scale * pos

Pos is the original position of the corner before rotation.

This'll give you the screen position of the corners.

\$\endgroup\$
  • \$\begingroup\$ Yes, I would also need a projection matrix to get the coords from 3d to 2d space. But how can I guarantee that the viewport of the camera never goes outside the world? Specifically in Unity. \$\endgroup\$ – Simian May 5 '17 at 3:59
  • \$\begingroup\$ @Simian It's very hard to stop it at the corners, but for example a game a played a long time ago had an oversized world and didn't let you to go further than a specific poimt \$\endgroup\$ – Bálint May 5 '17 at 6:48
-1
\$\begingroup\$

You need to check the X coordinates. In the above picture 50 units is the center i'm guessing and 100 units would be maximum. So check if your camera scrolling more than 100 units or less than 0 units. If the condition is true, stop scrolling.

\$\endgroup\$
  • \$\begingroup\$ In 3D space, it is not anywhere as simple as you explain. \$\endgroup\$ – Gnemlock May 7 '17 at 6:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.