-->
The camera defines how our graphics are rendered. The way this happens is by "projecting" the data. Normally this projection calculation involves some combinations of matrices. Luckily Unreal Engine does all this math for us behind the scenes (did you get it?), so we don't have to worry about this. However, we have to worry about which kind of projection we need. Generally speaking, in game development we only use either Perspective Projection or Orthographic Projection. Perspective projection is normally used in 3D games, where objects that are farther away are rendered smaller than those closer to the camera. Orthographic projection does not make size distinction with distances. This technique is commonly used for 2D games and is exactly what we will use.
In Unreal Engine, the orthographic projection provides a few setting values and probably the most important for us is the OrthoWidth
, which will define the dimensions of the viewport and, of course, how much we will be able to see.
For our needs we will create a camera class, derived from ACameraActor
. One of the things our class has to do is to automatically compute the OrthoWidth
value to better fit whatever we have to display. In order to do that, the first thing is to understand what this setting does in practice. Suppose an image that is 650 pixels wide (forget for now about the height) and your camera (setup with Orthographic Projection) is pointing directly to the center of this image. Setting the OrthoWidth
to 650 means that the image will be mapped in a way to completely fill the width of the viewport. If instead the OrthoWidth
is set to 640, then the result is that some pixels will be cropped on each side. Finally, if the OrthoWidth
is set to 700 then black borders will appear on each side of the image. In other words, the OrthoWidth
can be seen as a mapping value, telling how many pixels should be used to be projected onto the viewport's width. Note that this will most likely scale the image but the benefit is that no matter the resolution of the game window is, the desired look can be kept if correctly setting the OrthoWidth
value.
To compute this value, the first thing that comes to mind is the algorithm used to scale an image within some constraints while still maintaining the the original aspect ratio (resize an image to be used as thumbnail, for example). So, let's (quickly) analyze a little bit of the math behind this algorithm.
First let's define a few things:
We want to find a scale value () such that results in the following conditions:
Condition 1 may seem to "overlap" condition 2. But there is a slightly difference here. Both conditions 1 and 2 must be met. Another way to say what this means is that at least one of the resulting dimensions must match the containing box but none of them can be bigger that the box itself.
We then begin by applying our initial scale to match the first condition, which is . Unless both the image and the box have the exact same aspect ratio, there will be two different scale values, one that will meet both conditions and the other will not:
If then it means the width difference between the image and the box is bigger than that of the heights. In this case, if we scale the image by then the resulting height will be bigger than the containing box. On the other hand, if we scale the image by then the resulting height will be the exact same of the containing box while the width will be smaller. Both conditions were met.
Conversely, if then the height difference between the image and the box is bigger than that of the widths. Following the same logic, if we scale the image using then the resulting image will have height matching the containing box but the width will be bigger. Scaling the image by will result in an image with height smaller than the containing box and width matching said box.
In any case, the resulting image will be contained inside the constraints box while still keeping the correct aspect ratio.
In our case what we actually want is to find a value to be set into the OrthoWidth
so that in its projection calculation the entire image will be displayed, with the correct aspect ratio. Remember that this value indicates how Unreal Engine map the viewport's contents into the screen, not a direct scale of the images. We then have to find an scale value () that when applied to the viewport size will result in a "box" big enough to wrap the image.
If we take
Then we can find such that:
Attention to the condition 2 as we are scaling "the box", not the image directly. We end up with:
In a similar analyzes, if then the difference between widths is bigger. Scaling "the box" by will result in it having the width matching that of the image and height bigger than the image. That's exactly what condition 2 is! In short, if then OrthoWidth
= otherwise OrthoWidth
= .
All this discussion shows that one of our necessary input data includes the dimensions of the background image. From this we know that the class will need to hold two values, width and height of this image. A method will be added so those values can be manipulated in order to give some flexibility while developing the game. Although we did define an specific size 720x1280
for the background image, if there comes any need to change this, there wont be anything to change in the camera class' code, just a few property value changes.
We will also add to the class some means to be "notified" about game window resizes so we can update the OrthoWidth
value which will ensure the background image and PlayField (and the blocks, of course) are correctly rendered on screen.
Ok, enough talk, let's add our custom camera class to the project. Besides the boilerplate code, we add a few extra things. We need a function that will request the camera to update the internal math, which must be called whenever the game window is resized. Internally we hold the dimensions of the background image, while providing a public function to change its value. And then an internal function that we will use to obtain the viewport dimensions so we can apply our math:
#pragma once
#include "CoreMinimal.h"
#include "Camera/CameraActor.h"
#include "ColCamera.generated.h"
UCLASS()
class UCOLUMNSTUTORIAL_API AColCamera : public ACameraActor
{
GENERATED_BODY()
public:
AColCamera();
// Means to "tell" the camera about game window resizes
// In other words, this should be called when the game window is resized
UFUNCTION(BlueprintCallable)
void UpdateCamera();
// Means to "tell" the camera about the dimensions of the background image
UFUNCTION(BlueprintCallable)
void SetBackgroundImageSize(int32 Width, int32 Height);
private:
FVector2D GetViewportSize() const;
// Internally hold the background image dimensions
UPROPERTY(EditAnywhere, meta = (DisplayName = "Background Image Size"))
FVector2D mBackgroundImageSize;
};
We now have to define (implement) the class itself. First, the include list:
#include "ColCamera.h"
#include "Engine.h"
In the constructor we perform some initialization. First, disable the Tick()
, since we won't need constant updating throughout the game. Then we make sure the RootComponent
is flagged to be hidden. Moving on, the background image vector has to be initialized, with the default value of 720x1080. Lastly, we have to setup the camera component. In this case, set its projection mode to Orthographic
, a default value to its OrthoWidth
and then set the bConstrainAspectRatio
flag to false
. This last flag needs some explanation. Quoting from the tooltip that is shown inside the editor is enough explanation:
If bConstrainAspectRatio is true, black bars will be added if the destination view has a different aspect ratio than this camera requested.
The mentioned black bars are rendered, meaning that those will crop the image depending on the window size. Of course we don't want that! Disabling this flag will still give us "black bars", but as a result of empty space. If desired that space could be filled with something. Note that we won't do this in this project. Anyway, the code:
AColCamera::AColCamera()
{
PrimaryActorTick.bCanEverTick = false;
RootComponent->bVisible = false;
RootComponent->bHiddenInGame = true;
mBackgroundImageSize = FVector2D(720, 1080);
GetCameraComponent()->SetProjectionMode(ECameraProjectionMode::Orthographic);
GetCameraComponent()->OrthoWidth = 256.0f;
GetCameraComponent()->bConstrainAspectRatio = false;
}
One thing to note is that although this seems to be hard coding values, those are only the default ones. Later on we will manipulate them.
The logic behind UpdateCamera()
has already been explained, all we have to do is apply the math, that is, "translate" that into C++:
void AColCamera::UpdateCamera()
{
const FVector2D viewport_size = GetViewportSize();
const float width = mBackgroundImageSize.X;
const float height = mBackgroundImageSize.Y;
const float s1 = width / viewport_size.X;
const float s2 = height / viewport_size.Y;
GetCameraComponent()->OrthoWidth = (s1 < s2 ? viewport_size.X * s2 : width);
}
The implementation of SetBackgroundImagesize()
does not require any explanation, does it? All we do in there is an assign:
void AColCamera::SetBackgroundImageSize(int32 Width, int32 Height)
{
mBackgroundImageSize = FVector2D(Width, Height);
}
Finally, we implement the method to retrieve the viewport size, GetViewportSize()
. In there we begin with a default return value, a 2D vector set to (1, 1). Then we obtain the game viewport through the GEngine
global object and utilize the viewport's GetViewportSize()
:
FVector2D AColCamera::GetViewportSize() const
{
FVector2D retval(1, 1);
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->GetViewportSize(retval);
}
return retval;
}
That's it. Build the project and then inside Unreal Engine Editor you should be able to drag a ColCameraActor
into the scene:
Let's start adding some elements to our scenes (both maps, MainMenu
and InGameTraditional
). Just drag the Col Camera actor to the scene and then set its location to (-200, 0, 0) and rotation to (0, 0, 0). This will greatly help us ensure the camera will point directly into the center of the background image. If we don't do this, the math used in here will not give the desired result.
The next setting we have to change is the Auto Activate for Player
which is, by default, Disabled. Change it to Player 0. Hopefully this is self explanatory! Even though, I'm quoting here the tooltip for this setting:
Specifies which player controller, if any, should automatically use this Camera when the controller is active.
We need a background image placeholder now. Placeholder because it will be replaced at a later moment and is meant just for testing purposes. Feel free to work on a "definitive" image if you desire! The thing is, later on when working of the themes system, we will see more details related to this.
As I have talked in the introduction, the tool I have chosen to develop some of the images for the game is GIMP. Instead of detailing each step I have taken, I will just describe what I wanted in the end so I could perform the tests: a 720x1080 grey image with yellow borders, 8 pixels for each border. Later on this will help check if the results are correct or not. In any case, import the image into the Textures
directory in Unreal Engine Editor. As a reference, the image I have used:
We can't directly use this texture. Rather, we need an Sprite. In other words, create a new Sprite asset (it's under the Paper2D category) named SP_BackgroundPlaceholder
. Edit it and in there set the Source Texture
property to the texture you have just imported. Save the change and close the sprite asset editor as there isn't anything else to be done. We can now drag this sprite directly into the scene, which will create an instance of PaperSpriteActor
. Set the actor's location to (0, 0, 0).
Observing the result of the scene, you will probably notice the fact that the camera is not pointing to the "face" of the image, but rather to its side. At this moment we have to make a choice:
This is a rather important choice since it will directly impact how we will have to code movement and block spawning later on. If we choose 1, then sideways movement will be performed by translating over the X axis of the blocks. Otherwise, if we choose 2, then the same kind of movement will require translating over the Y axis. In any case, vertical movement will be achieved by translating over the Z axis.
There is another detail here, and that is the fact that Unreal Engine spawns new sprite images with the same orientation of the background image that we see right now. This means that if we choose 2, we will have to also rotate every block that we spawn during the gameplay.
Choosing 1 means that we will have to correct a previous "mistake" in the camera positioning and rotation. I have done this on purpose as means to tell that when implementing a game we will always come across situations like this, where minor (sometimes huge) mistakes will have to be corrected. In this case, luckily it's very small and simple to correct. To be honest maybe this can't even be called a "problem" but nevertheless, know that it's very common for the first (or second, or...) try to not be exactly the desired one.
It's time to choose, right? For the rest of the tutorial I will assume choice 1, simply because it will feel more natural to use the X axis to move blocks sideways on the screen, not to mention the fact that we will somewhat have less work to do when spawning new blocks. So, let's correct the camera. Select it and update the transformation to Location (0, 200, 0) and Rotation (0, 0, -90).
If you are like me you are very eager to see something. I encourage you to click the "Play" button inside the editor. Ok... what do we see? Just a window filled with grey, nothing else! We can't even see the yellow borders of the placeholder image. And that's correct! If you look into the camera code, you will notice the fact that we have set the default value for its OrthoWidth
to 256. That's way smaller than our image's width, which is 720! So, just for some testing, select the came and update the OrthoWidth
value to match the image.
Hit "Play" again and... nice, we can see the side borders of the image! If you have followed all of the same values shown through the tutorial and is not testing in a new window then the entire background image will be displayed. However if you are testing in the selected viewport mode then chances are high the top and bottom sections of the image are cropped. That's because we are not using the ortho widget calculation yet. We will do so soon.
Remember to add (and setup) the camera as well as the background image on both maps.
By now we have a placeholder background image that is being shown by our camera. However, depending on the situation the image will be cropped and if we resize the game window, we notice that the background has fixed size, even though we did add code to correctly set the OrthoWidth
in a way that should display the entire image while keeping its correct aspect ratio. The problem here is that we are not "telling" the camera to actually update! When the game begins we want to call UpdateCamera()
. The easiest in this case is to override the BeginPlay()
function and from it call the necessary function. For that we first have to declare the override, in the public section of the AColGamera
class:
void BeginPlay() override;
Then, we implement it by calling the UpdateCamera()
and then calling the super class' BeginPlay()
so any blueprint derived class can use this function's node:
void AColCamera::BeginPlay()
{
// Make sure the camera has the right settings
UpdateCamera();
// And use the BeginPlay of the super class, allowing blueprint classes to use this function
Super::BeginPlay();
}
If you build the project and test it now, you will see that indeed we do get the camera to be updated in a way that will fully display the background image. However, if you resize the window you will notice that it's not being updated. Indeed, the "window resized event" system we have implemented in the last part is not being used. To "fix" that we have to create a delegate, bind UpdateCamera()
to it and then add this delegate into the game instance's multi cast delegate. Since we will need the game instance pointer, we have to include its .h in the camera's .cpp file:
#include "ColCamera.h"
#include "Engine.h"
#include "ColGameInstance.h"
The BeginPlay()
is the perfect place to perform this delegate binding since it occurs at the very beginning of the game and only once when the map is loaded. In there we have to obtain the game instance pointer, cast to UColGameInstance
and then use ListenWindowResized()
in order to add the delegate:
void AColCamera::BeginPlay()
{
// Make sure the camera has the right settings
UpdateCamera();
// Hook the UpdateCamera() function into the resized event handling
if (UColGameInstance* gi = Cast<UColGameInstance>(GetGameInstance()))
{
FOnWindowResizedDelegate wr_del;
wr_del.BindDynamic(this, &AColCamera::UpdateCamera);
gi->ListenWindowResized(wr_del);
}
// And use the BeginPlay of the super class, allowing blueprint classes to use this function
Super::BeginPlay();
}
Note in this code that we do have to call the UpdateCamera()
even if the event will also call this function. The reason for that is because map transitions will reset the camera and the window resized event will not be triggered. If you build the project now and test, the background image will remain fully displayed even if the game window gets resized. An slightly resized game window should result in this:
In this part, we have implemented the means to actually start seeing something on the screen. Although it isn't much, we at least now have the background image correctly displayed and updated in case the game window gets resized. In the next part we start implementing the grid itself.