Jarret.Byrne

Politics, Sociology, Firefighting, Technology, and Original Fiction

Unity3d RTS Camera Part 1: Data Component

This is part one of a multipart discussion of the camera system I have constructed for my experimental RTS game using Unity3d. We begin with building a class to house all of the data for the camera that all of the various parts of the system – zoom, rotation, panning – will need to share or impact. By separating out the data from the classes that consume them, we gain some logical division, and the centralization allows for greater control. There is also a certain degree of cleanliness to the system as well, as it helps to cut down on bugs when our operations are essentially “dumb”, and there is one data class that can be “smart” for all of them equally.




Create the Data Component

Start by creating a new C# class in Unity3d and name it CameraData. Where you put the script is irrelevant, but for the sake of keeping things organized, I put my in <project root>/Scripts/Camera. I like to keep my scripts in a separate folder, and I like to keep scripts related to a similar function together. To each their own.

Target Properties

The camera system I devised relies on a game object known as target. The idea is that the camera can be a child of the target so that we can control panning by moving the target (and thus its child, the camera, as well), while zooming and rotation can be handled separately by positioning the camera relative to the target. We can also point the camera at the target, thus using the target to determine the angle of view based on the camera’s position relative to the target.

I chose this method because panning the camera, while at the same time moving the camera for zoom, while at the same time moving the exact same camera object for rotation was a headache. Separating the panning calculations from  the zoom and rotation calculations was less of a headache.

So, since we will need a target, we will need some properties:

The target property will contain a reference to our target game object, which should be the camera’s parent. You can set this up in the hierarchy, but don’t worry, we’ll add a method that will create a parent target for the camera if it doesn’t have one later so you do not have to manage the hierarchy if you don’t want to.

The startPosition property is the location in the game world that the target game object should be placed in the event that we have to create one automatically. It will come into play when we add that method that creates a target game object if the camera has no parent later.

Zoom Properties

I want my camera to be able to zoom in and out, but only within acceptable bounds. The system defines a base distance that represents a standard level of zoom, and then a percentage is applied to that standard distance to determine how far the camera should be from the target. At 100%, the camera is at the standard level of zoom. At 50%, the camera is zoomed in to half the standard distance from the target. At 200%, the camera is zoomed out to twice the standard distance from the target.

The zoom property represents what percentage of the standard distance the camera should be from the target. We default it to 1 as this represents the standard zoom.

The stdDistance property represents how far the camera should be from the target. The current zoom property value will be applied to this value and the result will be how far the camera should be from the target.

The minZoom property represents the maximum level of zoom allowed. The “min” represents the “max” because as zoom is decreased, distance is increased. There is an inverse relationship between zoom and distance.

The maxZoom property represents the minimum level of zoom allowed. The “max” represents the “min” because as zoom is increased, distance is decreased. There is an inverse relationship between zoom and distance.

Rotation Property

The camera needs to rotate around the target, and for that we will need to keep track of the rotation of the camera.

The rotation property represents the angle at which the camera should be rotated around the target, in degrees. Here the rotation is defaulted to -90 degrees (equivalent to 270 degrees). -90 degrees is a 0 on the x-axis and a -1 on the y-axis on the unit circle. Keep in mind that rotation will actually use the z-axis for the y-axis, but that will make more sense later.

Pan Properties

While panning is designed in the system to happen independently of zooming and rotation, it nonetheless stories its data in the Data Component along with zoom and rotation.

The panBuffer property represents the percentage of the screen from the edge that will trigger panning if the mouse moves into that space. Basically, it defines how close to the edge of the screen the mouse has to be in order to trigger panning.

The _panBufferLeft, _panBufferRight, _panBufferBottom, and _panBufferTop properties are going to hold the points on the screen that panning begins. The values are computed once and stored as to cut down on overhead. The properties will be used to determine when scrolling should begin or end later. For example, if the mouse position’s x-value  is less than the _panBufferLeft value, then we should be panning left. The properties are private because only the Date Component will use them.

The panDistance property represents how far the camera should move per fixed-update-frame when the camera is actively panning. Here it is set to 5 meters by default.

The panSpeed property represents the speed at which the camera should move when panning. This property is also used to determine the speed of zooming and rotating as well. Perhaps a better name is transitionSpeed, but this is fine for now.

The last property, heightRayLength, contains the distance that the target will cast a ray in order to find the ground. This property comes into play later when the camera is panned across terrain that is not perfectly flat and therefore must adjust its y-position to account for the change in height.

Dynamic Properties

The Data Component makes use of a few properties that do not contain static data. Instead these properties return different values depending on the other available static data. Could have just as easily been functions as they only have getters, but I preferred dynamic properties for this implementation.

All three of the properties above compute the coordinates of the camera relative to the target, taking into account both the rotation and the zoom level simultaneously. We will use them when positioning the camera with the right zoom and rotation.

The xDistance and zDistance properties are where the rotation is applied. The xDistance property uses the cosine of the current rotation to determine the x-axis value of the rotation on the unit circle. The zDistance property uses the sine of the current rotation to determine what would normally be the y-axis value of the rotation on the unit circle.

The Start Method

Unity provides the Start method as a part of its core MonoBehavior class. We can to use it to setup some initial properties that will be needed as the camera is manipulated.

The first thing we do is call a method that we will define next – storeTarget. That method will handle getting and/or creating the parent target.

The block at the end handles computing the screen locations where panning will begin/end if the mouse should move into those zones. The screen’s width and height are taken into account, along with the buffer value set earlier.

The StoreTarget Method

For the sake of sanity, and also for safety, I wanted my camera script to be able to survive if I should forget to place it inside a game object that would serve as its target. I don’t like the idea that the DataComponent script, and therefore all the other scripts, would be dependent on me taking action in the UI when that action is simple and easily forgotten. The StoreTarget method handles getting and, if necessary, creating the parent game object which will be the camera’s target.

The StoreTarget method begins by checking if the camera has a parent. If the camera does not have a parent, then a new empty game object is created, given the name of “Camera Target” for the sake of easily finding it in the hierarchy later if needed, the camera is made a child of the new game object, and then a reference to the new game object is stored as the camera’s target. If the parent already exists, then all it has to do is store a reference to it.

The ShouldMove Methods

I’m a big fan of convenience methods because they centralize decisions and tend to keep code clean. For the purposes of my camera, I decided the Data Component should decide if it is time to pan or not, and created 4 convenience methods to gain insight from the Data Component.

For each of the convenience methods I am checking two things: is the mouse inside of the pan-zone for that direction; and is the player using the keyboard to control the camera’s panning. With four quick, centralized methods, I now can let the player pan with both their mouse and the keyboard, and functionally they can be treated just the same later.

The ShouldRotate Methods

Like I did for the 4 ShouldMove methods, I created convenience methods for whether the camera should rotate.

The code above represents non-final code. I haven’t quite decided how the player will trigger a rotation yet, only that I want them to be able to trigger it. As a result, the Axis I used above are not likely at all the ones you will want to use, but they allowed me to successfully test my camera, and that was of value. Change the Axis or conditionals as you need to; so long as you return a boolean, the system will do what it needs to just fine.

The GetGroundHeight Method

For now, this is the last method in the DataComponent script, and certainly the most complex. If I were making a classic 2d RTS, the camera would never need to worry about the height of the terrain because it would be uniform and flat. In a dynamic 3d universe, however, the ground beneath the camera can rise and fall, and this impacts the camera – or at least I wanted it to. The goal was to eliminate, as much as possible, the condition where the terrain rises above where the camera is facing, which should be at our target. For this reason, the target needs to rise and fall with the terrain, and I need to know where the terrain is below – or perhaps above – the target.

Figuring out where the terrain is requires that we cast a ray in hopes that the ray will hit it. We start by creating a RaycastHit variable that will hold the collision information in the event that the ray does actually find our terrain. The second line defines a layer-mask for the 8th layer, which I named “Terrain” and assigned to my terrain. The idea is that I did not want the ray to think that a building or a unit was the ground – only the ground is the ground. I use the layer-mask to tell the ray to ignore anything but, well, the ground. If you use this exact method, you will either need to add that layer, or remove the layer-masking and implement your own idea. If you have a better one than I did, please let me know in the comments.

You may notice that two rays might be cast. The first one casts a ray downwards, thinking that most often the ground will be below the target. If this is the case, the location of the ground below the target is returned. I added one to the height of the terrain’s height because if the target is set to exactly the height of the ground, we will never find the ground again with our raycasts. In the event the ground is not found below the target, a ray is cast upwards, thinking that perhaps the target has, through miscalculation or easing, fallen below the terrain.

If the terrain just cannot be found, I opted to throw an exception because this is a huge problem and needs to be addressed (which I will later, when the game progresses).

The Whole Script

For reference, here is the whole DataComponent script, all put together:

Conclusion

So this concludes Part 1 of the RTS Camera System series that I am working on. The other parts are still pending and will be linked up just as soon as they are ready. Part 3 is still pending, but Part 2 in the series is now available The second and third parts are now available at Unity3d RTS Camera Part 2: Zoom and Rotate and Unity3d RTS Camera Part 3: Panning.

Previous

Don’t Leave a Man In the Chimney

Next

Unity3d RTS Camera Part 2: Zoom and Rotate

6 Comments

  1. Roberts

    Great stuff. Will definitely follow this, as most camera scripts seem useless to me, but your one stands out a bit better. Going to wait for next part and test this thing out now.

  2. This looks awesome. Hopefully my buddies and I have a need for this in whatever project we end up deciding to do. 🙂

  3. GS

    Hey Jarret, thanks for sharing this!

    (Before I go on, though, it seems there’s a little mistake in your scripts; the CameraData script has the method “storeTarget” but it’s “StoreData” that is called in other scripts, so that leads to an error)

    I stumbled upon your posts while researching for building a camera that suits my project; I’m fairly noob both in code and unity, so my apologies in advance if my question seems dumb but I can’t seem to configure everything correctly to get your scripts to work.

    So what I’m trying to achieve is a camera that will be used in an isometric view (so the zoom script should use the projection size rather than change the coordinates), and I’d like for it to rotate only by set amounts everytime the input is pressed (90° by 90° around the focus point), with a nice lerp.

    Lastly, the camera should be able to toggle its focus point from the player game object, to a focus point that can be moved freely around via player input (the idea being to be able to pan, zoom and rotate when you’re in “free mode”, and get back to the player object when a button is pressed).

    I think your scripts provide an excellent (and most importantly, tidied up) way of doing what I need, but I can’t seem to alter them to account for the behavior I’m looking for.

    If you happen to have the time for it, and would be willing to help me, that would be super kind of you 🙂

    Thanks!

    • GS

      (I misstyped “StoreData” when I meant “StoreTarget”, sorry ;p)

    • jbyrne

      Hey there GS,

      Thanks for the comment. You were absolutely correct about the capitalization issue on the storeTarget method – I’ve gone and updated all the calls to that method to use the right name. Thanks for the heads-up on that!

      As for the camera system you are hoping to build, if you break it down into its individual parts and try and knock them out one by one, it certainly seems doable to me, albeit tricky. Specifically what the system would look like I’m afraid I don’t know without first trying to build it myself, but I can at least try and explain how I might go about it, hopefully that will be useful.

      Since the rotation part seems the most straight forward I would likely start there. In the CameraRotate script, within the FixedUpdate method, we make the decision to rotate the camera using the CameraData::shouldRotateLeft() and CameraData::shouldRotateRight() methods. If either returns true, we increment or decrement the rotation value by 1 degree as necessary. To achieve the 90-degree rotations you’re looking for, increment or decrement by 90 degrees instead of 1 degree.

      You will need to adjust CameraData::shouldRotateLeft() and CameraData::shouldRotateRight() to make sure they can’t return true every single frame like they currently do, otherwise, well, that’s a whole lot of rotation. As I have written it, holding the button down is how you rotate, but you’ll probably be more interested in listening for when the button is pressed – meaning when the button is pressed down, and then allowed to come back up. Take a look at Unity’s built in GetButtonUp method: http://docs.unity3d.com/ScriptReference/Input.GetButtonUp.html. Note: if you go this route, change CameraRotate to use Update instead of FixedUpdate!

      The rotational lerp’ing is built into this system, so I think that might be all you need to do there.

      For the isometric camera zooming, I’m afraid I don’t have a ton of experience there, but my guess is that in the CameraZoom class, instead of changing the distance between the camera and the target, you’d have a separate property for…I think it’s depth of field? Again, not my strong suit, but much of the methodology of my system (which is by no means perfect, by the way), should support this need, although it may require tweaking or a new method/property or two.

      The last part about two cameras is tricky. I’ve never really switched between cameras, but I know Unity supports multiple cameras in a scene at once, though it only projects one as the main camera at any one time.

      The first thing I would try is having two cameras, each with their own CameraData class containing their own properties, so they can have zoom and rotation independent of each other. One camera I would put as a child of the GameObject representing the player, the other can go in the root of the scene hierarchy, as it will automatically create a parent to live within.

      You would need a separate object to contain references to all of our main cameras to control which to turn on and which to turn off and when. Here’s a thread on the Unity forums that may be helpful: http://answers.unity3d.com/questions/16146/changing-between-cameras.html.

      Hope that helps get you started!

      Take care,
      – Jarret

Leave a Reply

Powered by WordPress & Theme by Anders Norén