Introduction
Hello everyone, this post is about setting up an Isometric Camera Controller for the Godot Engine. This controller enables us to grab, rotate, zoom, and edge pan the camera similar to other Real-Time Strategy (RTS) games. For this guide, I will be using Godot version 4.2.2, and the scripting language will be C#.
Isometric Camera Setup in Godot Editor
First, create a new project and set up a basic scene with a plane and cubes. Then, add a Camera3D node. If you select the Camera3D node, you will see a preview button which, as the name implies, gives you a preview of the camera view.
Then, under Inspector -> Camera3D properties, make sure that the projection is orthogonal, which ensures that objects remain the same size on the screen no matter how far away they are. The “Size” property is used to adjust the camera size and can be used for the zoom in/out feature.
Currently, if you look at the preview, you will see nothing being projected. To make it work, we have to modify the Transform Properties, especially the y-position and x-rotation. The y-position moves the camera up or down depending on the value and can be used to avoid clipping. Here, a rotation of x=-90 gives a flat orthogonal view, while x=-45 gives you an isometric view. You can easily adjust the angle for your desired camera view or effect.
Now, after finishing the camera setup, let’s move toward the camera controller script.
Isometric Camera Controller Script
Since we will be panning and rotating the camera, we don’t want to pan/rotate the actual camera node. Instead, make a new 3D Node and name it appropriately; for this guide, I will name it ‘CameraHolder’. Make the Camera3D a child of the ‘CameraHolder’ node. Create a new script called ‘CameraController’ and attach it to the ‘CameraHolder’ node and open it.
public partial class CameraController : Node3D
{
private readonly float _edgePanningTriggerOffsetValue = 2f;
private readonly float _panningCameraSpeed = 50f;
private readonly float _middleMouseGrabCameraSpeed = 5f;
private readonly int _zoomMinDisatance = 20;
private readonly int _zoomMaxDisatance = 60;
private readonly int _zoomAmountPerRoll = 10;
private float _currentSize = 0f;
private float _smoothTimer = 0.0f;
private float _smoothZoomTime = 1.0f;
private float _screenRatio;
private Vector2 _size;
private Vector2 _mouseRelativeVel = Vector2.Zero;
private bool _applyZoomSmooth = false;
private bool _rotateCamera = false;
private bool _isDragging = false;
[Export] private Camera3D _camera;
public EdgePannigState pannigState = EdgePannigState.None;
public override void _Ready()
{
_currentSize = _camera.Size;
_size = GetViewport().GetVisibleRect().Size;
_screenRatio = _size.X / _size.Y;
GetViewport().SizeChanged += SizeChanged;
Input.MouseMode = Input.MouseModeEnum.Confined;
}
public override void _ExitTree()
{
GetViewport().SizeChanged -= SizeChanged;
}
private void SizeChanged()
{
_size = GetViewport().GetVisibleRect().Size;
_screenRatio = _size.X / _size.Y;
}
public override void _Process(double delta)
{
if (_isDragging)
{
GlobalPosition -=
GlobalTransform.Basis.X * _mouseRelativeVel.X * _middleMouseGrabCameraSpeed * (float)delta +
GlobalTransform.Basis.Z * _mouseRelativeVel.Y * _middleMouseGrabCameraSpeed * (float)delta * _screenRatio;
}
else
{
ApplyPanningStateToCamera(delta);
}
if (_applyZoomSmooth)
{
ApplyZoom(delta);
}
if (_rotateCamera)
{
RotateY(-_mouseRelativeVel.X * 0.5f * (float)delta);
}
//relative mouse velocity need to be reset after every frame
_mouseRelativeVel = Vector2.Zero;
}
private void ApplyZoom(double delta)
{
_smoothTimer += (float)delta;
if (_smoothTimer < _smoothZoomTime)
{
_camera.Size = Mathf.Lerp(_camera.Size, _currentSize, _smoothTimer / _smoothZoomTime);
}
else
{
_applyZoomSmooth = false;
}
}
private void ApplyPanningStateToCamera(double delta)
{
switch (pannigState)
{
case EdgePannigState.None:
break;
case EdgePannigState.Left:
GlobalPosition +=
GlobalTransform.Basis.X * -_panningCameraSpeed * (float)delta;
break;
case EdgePannigState.Right:
GlobalPosition +=
GlobalTransform.Basis.X * _panningCameraSpeed * (float)delta;
break;
case EdgePannigState.Up:
GlobalPosition +=
GlobalTransform.Basis.Z * -_panningCameraSpeed * (float)delta;
break;
case EdgePannigState.Down:
GlobalPosition +=
GlobalTransform.Basis.Z * _panningCameraSpeed * (float)delta;
break;
}
}
public override void _Input(InputEvent @event)
{
if (@event is InputEventKey keyEvent)
{
if (keyEvent.Keycode == Key.Escape && keyEvent.Pressed)
{
Input.MouseMode = Input.MouseMode == Input.MouseModeEnum.Confined ? Input.MouseModeEnum.Visible : Input.MouseModeEnum.Confined;
}
}
if (@event is InputEventMouseButton mouseButton)
{
switch (mouseButton.ButtonIndex)
{
case MouseButton.WheelDown:
case MouseButton.WheelUp:
{
if (!_isDragging)
{
_currentSize = mouseButton.ButtonIndex == MouseButton.WheelUp ? _currentSize - _zoomAmountPerRoll : _currentSize + _zoomAmountPerRoll;
_currentSize = Mathf.Clamp(_currentSize, _zoomMinDisatance, _zoomMaxDisatance);
_smoothTimer = 0;
_applyZoomSmooth = true;
}
}
break;
case MouseButton.Middle when mouseButton.Pressed:
_isDragging = true;
break;
case MouseButton.Right when mouseButton.Pressed:
_rotateCamera = true;
break;
default:
_rotateCamera = false;
_isDragging = false;
break;
}
}
if (@event is InputEventMouseMotion mouseMotion)
{
_mouseRelativeVel = mouseMotion.Relative;
EdgePanningDetection(mouseMotion.Position);
}
}
private void EdgePanningDetection(Vector2 mouseMotion)
{
if (mouseMotion.X < _edgePanningTriggerOffsetValue)
{
pannigState = EdgePannigState.Left;
}
else if (mouseMotion.X > (_size.X - _edgePanningTriggerOffsetValue))
{
pannigState = EdgePannigState.Right;
}
else if (mouseMotion.Y < _edgePanningTriggerOffsetValue)
{
pannigState = EdgePannigState.Up;
}
else if (mouseMotion.Y > (_size.Y - _edgePanningTriggerOffsetValue))
{
pannigState = EdgePannigState.Down;
}
else
{
pannigState = EdgePannigState.None;
}
}
}
public enum EdgePannigState
{
None,
Left,
Right,
Up,
Down
}
Lets breakdown the above code:
Variables | Constants
- _edgePanningTriggerOffsetValue:
- A constant defining the threshold distance from the edge of the screen to trigger edge panning, meaning that if it has a value of 5, edge panning would start 5 pixels before reaching the edge.
- _panningCameraSpeed:
- This is a speed at which the camera move when using edge panning.
- _middleMouseGrabCameraSpeed:
- This define a speed at which the camera move when the middle mouse or grab button is held down.
- _zoomMinDistance, _zoomMaxDistance:
- These are the constants defining the minimum and maximum zoom distances for camera.
- _zoomAmountPerRoll:
- This is a amount by which the zoom changes per mouse wheel roll.
- _currentCameraSize:
- This give the current camera size used for zooming in/out.
- _smoothTimer, _smoothZoomTime:
- These are used for smooth zoom interpolation.
- _screenRatio, _viewPortSize:
- These are used to store the screen ratio and size of the viewport.
- _mouseRelativeVel:
- This give the relative mouse velocity whenever mouse is moved.
- _applyZoomSmooth, _rotateCamera, _isDragging:
- These flags are used to control zooming, camera rotation, and dragging behavior.
- _camera:
- Reference to the Camera3D node.
- panningState:
- The current panning state (e.g., Left, Right, Up, Down, None) represented by Enum.
Methods
- _Ready():
- In this method we Initializes variables related to camera size, viewport size, and aspect ratio and subscribes to the viewport size changed event.
- _ExitTree():
- This is called when the node is removed from the scene tree. we usually unsubscribes to all event here to avoid memory leaks.
- SizeChanged():
- This is called when the viewport size changes and will updates the size and aspect ratio variables accordingly.
- _Process(double delta):
- Here we handles camera movement, zooming, and rotation based on user input and current panning states.
- ApplyZoom(double delta):
- This is used to apply smooth zooming by interpolating the camera size over time.
- EdgePanningDetection(Vector2 mouseMotion):
- This method detects if the mouse cursor is near the edges of the screen and sets the corresponding panning state (Left, Right, Up, Down, or None) based on the cursor position.
- ApplyPanningStateToCamera(double delta):
- This method applies the current panning state to the camera, moving it in the corresponding direction based on the panning camera speed.
- _Input(InputEvent @event):
- Here we handles various input events such as key presses, mouse button presses, and mouse motion. Controls camera zoom, rotation, and dragging behavior.
Conclusion
In this post, we’ve created an Isometric Camera Controller in Godot Engine using C#. You can easily integrate this controller into your project to enable smooth camera movements for your RTS game or any other 3D project.
Feel free to customize the script further to suit your specific requirements and enhance your game’s camera control functionality. Here is the Github link for this project.