Part 3 - Camera
April 11, 2019

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.

Some Math

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 OrthoWidthto 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 (ss) such that Rs=sIsR_s = s * I_s results in the following conditions:

  1. Rx=BxR_x = B_x and/or Ry=ByR_y = B_y
  2. RxBxR_x \le B_x and RyByR_y \le B_y

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 Bs=sIsB_s = s * I_s. 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:

s1Ix=Bxs1=BxIxs_1 * I_x = B_x \rightarrow s_1 = \frac{B_x}{I_x}s2Iy=Bys2=ByIys_2 * I_y = B_y \rightarrow s_2 = \frac{B_y}{I_y}

If s1>s2s_1 > s_2 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 s1s_1 then the resulting height will be bigger than the containing box. On the other hand, if we scale the image by s2s_2 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 s1<s2s_1 < s_2 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 s2s_2 then the resulting image will have height matching the containing box but the width will be bigger. Scaling the image by s1s_1 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.

Applying to OrthoWidth

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 (ss) 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 B=sVB = s * V such that:

  1. Bx=IxB_x = I_x and/or By=IyB_y = I_y
  2. Bx>=IxB_x >= I_x and By>=IyB_y >= I_y

Attention to the condition 2 as we are scaling "the box", not the image directly. We end up with:

s1=Ix/Vxs_1 = I_x / V_xs2=Iy/Vys_2 = I_y / V_y

In a similar analyzes, if s1>s2s_1 > s_2 then the difference between widths is bigger. Scaling "the box" by s1s_1 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 s1<s2s_1 < s_2 then OrthoWidth = Vxs2V_x * s_2 otherwise OrthoWidth = IxI_x.

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.

The Class

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:

ColCamera.h
#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:

ColCamera.cpp
#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:

ColCamera.cpp
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++:

ColCamera.cpp
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:

ColCamera.cpp
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():

ColCamera.cpp
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:

Col Camera Actor Object

Initial Scenes

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:

  1. Rotate the camera (and reposition it) so it will correctly point towards the "face" of the image.
  2. Rotate the image 90º around the Z axis.

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.

Notifying the Camera

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:

ColCamera.h
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:

ColCamera.cpp
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:

ColCamera.cpp
#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:

ColCamera.cpp
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.

Introduction
Previous12
3
4567891011121314151617181920212223Next