Wednesday, August 21, 2013

Customizing RTS Camera Behavior

So a few people have purchased RTS Camera Pro, which is awesome.  I'd love some feedback if anyone is using it (especially if it actually makes it into a production game).

And speaking of feedback...

One of the purchasers asked how he'd go about zooming and moving the camera using on-screen controls and zooming in via double-clicks (paraphrased, obviously).

The script below does this.   It's obviously raw demo-esque code (it took all of ten minutes to write and I didn't bother really commenting it very well).   The GUI is embedded in the script and would probably be broken out if this was for a "for realz" application, but this is fine for testing.

It allows:

  • Pan via left-mouse button down (click and drag)
  • Zoom on double-click
  • Movement and zooming via on-screen GUI

The only "tricky" part (and it wasn't really much of a trick) was detecting double-clicking.   This way works, but it can certainly be improved upon.

I also added checking to see if the user is inside the GUI area so that clicking in the GUI doesn't pan.   If you know a better way to do this, please let me know.

Just attach the script (below) to the Main Camera (or any other scene object, really) and it'll do the trick.

Here's the script:

 using UnityEngine;  
   
 public class CustomCamera : MonoBehaviour  
 {  
   private RtsCamera _rtsCamera;  
   private RtsCameraMouse _rtsCameraMouse;  
   private RtsCameraKeys _rtsCameraKeys;  
   private Rect _guiRect;  
   
   float _doubleClickStart = 0;  
   
   private bool _prevMouseDown = false;  
   
   void Start()  
   {  
     _rtsCamera = Camera.main.GetComponent<RtsCamera>();  
     _rtsCameraMouse = Camera.main.GetComponent<RtsCameraMouse>();  
     _rtsCameraKeys = Camera.main.GetComponent<RtsCameraKeys>();  
   
     // we're going to change our zoom speed programmatically, just for testing... should really do this in the Unity editor  
     _rtsCameraMouse.ZoomSpeed = 5000;  
     _rtsCameraKeys.ZoomSpeed = 200;  
   
     // start out zoomed ALL the way out... mainly because we're testing zooming in with double click  
     _rtsCamera.Distance = _rtsCamera.MaxDistance;  
   
     const int areaWidth = 350;  
     const int areaHeight = 200;  
     _guiRect = new Rect((Screen.width - areaWidth) / 2f, Screen.height - areaHeight - 64, areaWidth, areaHeight);  
   }  
   
   void Update()  
   {  
     // check to see if the user is pressing (holding) mouse button 0 (left mouse)  
     if (Input.GetKey(KeyCode.Mouse0))  
     {  
       if (!IsInsideGui()) // we ONLY pan when the user is NOT inside the gui area(s)  
       {  
         _prevMouseDown = true;  
   
         var panX = -1 * Input.GetAxisRaw("Mouse X") * _rtsCameraMouse.PanSpeed * Time.deltaTime;  
         var panZ = -1 * Input.GetAxisRaw("Mouse Y") * _rtsCameraMouse.PanSpeed * Time.deltaTime;  
   
         _rtsCamera.AddToPosition(panX, 0, panZ);  
       }  
     }  
     else  
     {  
       if (_prevMouseDown)  
       {  
         OnMouseUp();  
         _prevMouseDown = false;  
       }  
     }  
   }  
   
   /// <summary>  
   /// Checks to see if the mouse is within the gui area. We don't want to pass the left clicks through if it is.  
   /// </summary>  
   /// <returns>True if the mouse is currently inside the gui area, False otherwise.</returns>  
   private bool IsInsideGui()  
   {  
     var ix = Input.mousePosition.x;  
     var iy = Input.mousePosition.y;  
     var transMouse = GUI.matrix.inverse.MultiplyPoint3x4(new Vector3(ix, Screen.height - iy, 1));  
   
     return _guiRect.Contains(new Vector2(transMouse.x, transMouse.y));  
   }  
   
   /// <summary>  
   /// Render the simple test UI  
   /// </summary>  
   void OnGUI()  
   {  
     var t = Time.deltaTime;  
     var speed = _rtsCameraKeys.MoveSpeed;  
     var rotSpeed = _rtsCameraKeys.RotateSpeed;  
     var tiltSpeed = _rtsCameraKeys.TiltSpeed;  
   
     GUILayout.BeginArea(_guiRect);  
   
     GUILayout.BeginHorizontal("box");  
     GUILayout.Space(16);  
   
     GUIControls(speed, t, rotSpeed, tiltSpeed);  
   
     GUILayout.Space(16);  
     GUILayout.EndHorizontal();  
     GUILayout.EndArea();  
   }  
   
   private void GUIControls(float speed, float t, float rotSpeed, float tiltSpeed)  
   {  
     GUILayout.BeginVertical();  
   
     GUILayout.Space(16);  
   
     GUIMovementButtons(speed, t);  
   
     GUILayout.BeginHorizontal();  
     GUIRotateButtons(t, rotSpeed);  
     GUILayout.Space(16);  
     GUITiltButtons(t, tiltSpeed);  
     GUILayout.EndHorizontal();  
   
     GUILayout.Space(16);  
   
     GUIZoomSlider();  
   
     GUILayout.Space(16);  
   
     GUILayout.EndVertical();  
   }  
   
   private void GUIZoomSlider()  
   {  
     GUILayout.Label("Zoom");  
     _rtsCamera.Distance = GUILayout.HorizontalSlider(_rtsCamera.Distance, _rtsCamera.MinDistance, _rtsCamera.MaxDistance);  
   }  
   
   private void GUITiltButtons(float t, float tiltSpeed)  
   {  
     if (GUILayout.RepeatButton("Tilt Up"))  
     {  
       _rtsCamera.Tilt += t*tiltSpeed;  
     }  
     if (GUILayout.RepeatButton("Tilt Down"))  
     {  
       _rtsCamera.Tilt -= t*tiltSpeed;  
     }  
   }  
   
   private void GUIRotateButtons(float t, float rotSpeed)  
   {  
     if (GUILayout.RepeatButton("Rotate Left"))  
     {  
       _rtsCamera.Rotation += t*rotSpeed;  
     }  
     if (GUILayout.RepeatButton("Rotate Right"))  
     {  
       _rtsCamera.Rotation -= t*rotSpeed;  
     }  
   }  
   
   private void GUIMovementButtons(float speed, float t)  
   {  
     GUILayout.BeginHorizontal();  
   
     if (GUILayout.RepeatButton("Left"))  
     {  
       _rtsCamera.AddToPosition(-speed*t, 0, 0);  
     }  
     if (GUILayout.RepeatButton("Forward"))  
     {  
       _rtsCamera.AddToPosition(0, 0, speed*t);  
     }  
     if (GUILayout.RepeatButton("Back"))  
     {  
       _rtsCamera.AddToPosition(0, 0, -speed*t);  
     }  
     if (GUILayout.RepeatButton("Right"))  
     {  
       _rtsCamera.AddToPosition(speed*t, 0, 0);  
     }  
   
     GUILayout.EndHorizontal();  
   }  
   
   void OnMouseUp()  
   {  
     if ((Time.time - _doubleClickStart) < 0.3f)  
     {  
       this.OnDoubleClick();  
       _doubleClickStart = -1;  
     }  
     else  
     {  
       _doubleClickStart = Time.time;  
     }  
   }  
   
   void OnDoubleClick()  
   {  
     // get position the user double clicked (on plane)  
     var ray = Camera.mainCamera.ScreenPointToRay(Input.mousePosition);  
     var fDist = 0.0f;  
     RaycastHit hitInfo;  
     if (Physics.Raycast(ray, out hitInfo))  
     {  
       // we hit something!  
   
       // move the user to the double-clicked position  
       _rtsCamera.JumpTo(hitInfo.point);  
   
       // zoom in "about 25%"  
       var dist25Percent = (_rtsCamera.MaxDistance - _rtsCamera.MinDistance)*0.25f;  
       _rtsCamera.Distance -= dist25Percent;  
     }  
   }  
 }  
   

4 comments:

  1. Awesome,

    Thanks heaps for this John, it's just what I needed for my interactive map project.

    The additional script creates a navigation GUI ideal for web player projects designed for a userbase with no experience of, or interest in learning, keyboard short-cuts.

    The mouse control changes mimic those of Google Maps and Google Earth, which make them super easy and intuitive.

    Nigel H

    ReplyDelete
  2. I needed an RTS style camera for a combat tactics game I'm building and I found this. As others have stated, it works right out of the box, is super clean and easy to set up. Attention to details such as ease in/out are well handled and feel great.

    There are only two things I would request for updates:
    1. It would be nice if the camera itself had a sphere collider on it to detect penetration with high environment geometry, such as cliff walls, and translated the camera along its look vector to keep the cam inside the world.
    2. The cam bounds and initial placement variables work great. However, if your main camera is a prefab used throughout multiple scenes, this doesn't work since each scene requires unique bounds and starting locations. Any idea how best to handle that?

    Other than that, thank you for these camera tools. So useful!
    -Marcus

    ReplyDelete
  3. Just purchased from Unity Asset store. I used add component to add the RTS Camera script as stated in the read me. My main camera preview shows my scene just fine, but when I hit play I only see the skybox. I'm not a coder so any tips on getting this working properly would be greatly appreciated.

    Thanks

    ReplyDelete