Dev Log: Cameras all boil down to basic maths

When one gets buried in a problem, it's easy to forget the simple things. Sometimes you sit there scribbling down complex calculus, trying to find the sin of the sum of the square root of xm + yn all to the power of pi, divided by the volume of the nearest black hole measured in imperial units exclusively, only to find out all you were looking for was a + b. It's one of those moments where you kick yourself for missing it earlier, but you had to go through the hell you went through to actually arrive at the simple solution. So infuriatingly simple that you have an overwhelming desire to play the most expensive game of frisbee you can, and send your laptop crashing into the aforementioned black hole.

The problem

Using a camera on a game with a dynamic grid creates a very common problem found in real life cameras as well, given you're trying to photograph a hoard of kittens. Your subject refuses to stay still, or stay in focus, or even stay in frame. It will simply go in the direction it pleases, and bollocks to your wants and needs. That looks something like the below.

So, I need a way of keeping the grid in the screen. Specifically in the center of the screen, give or take a small offset. That's the intended output. So what's our parameters? Well, we have a grid. That grid is built on positive and negative integer numbers, and is a constantly adjusting size. Every time a square is added, the game will need to recalculate its position based on the size of the grid in both x and y axis.

Where to begin

Unity has a pretty clean approach to object interaction, and thanks to some totally not terrible code, I can add a reference to the camera object to the level runner script, and use the singleton approach from my grid manager to grab the details from the grid. So, now I needed to figure out how to calculate. As for my data structure I'm using Dictionary<(int, int), GameObject> so I can retrieve each cell by the grid co-ordinates. Fun fact, this also helps prevent duplicates appearing. It does however throw an error if it does try to do that. Every cloud has a buggy lining.

Don't code tired, friends

At some point I looked at the significant pile of spaghetti I've been working on intermittently for a while, and decided "You know what this needs? Complex maths. When has complex maths ever made life worse?". I glare across at my Computer Science degree, and decide to neglect the knowledge and experience it's provided me, and push forward with the complex maths. I start with averaging. I figure that the average of the grid in the x plus the average in y should put me in the middle of the grid. Seems reasonable, but this only going to work in 2 scenarios. If the grid was equal size in the positive and negative direction, resulting in an average of zero, or if maths decided to fuck itself purely for my entertainment. Long story short, this didn't work for the reasons that maths is constant. What's frustrating is that it came close, but falls apart for anything else. I figured that ((max-min)/number of blocks in a row) would be correct. I will wholly concede that my brain has lately felt like something akin to a fruit smoothie so it checks out that I'd be making mistakes like this. But it baffled me. Logic was "average length should work". I mean, that logic hasn't failed me before.

So I tried all kinds of complicated logic. Imagine just about every permutation of length of grid, width of the grid, maximum value, minimum value, with a liquorice assortment of operators thrown in. I couldn't crack it. Like a dreadful architect, I was once again going back to the drawing board.

The solution, part one

Believe it or not, the solution existed in maths that you're taught at around age 9. Maybe earlier. It's been a long time since I've been 9. Anyway. Mean. Like really, mean. As we know that the numbers will all be sequential for the keys, because the grid will always have a game object (even if it's an empty object), so they will always be a full sequence. So if we take the sum of all the grid coordinates, and divide it by the length of the row/column, and boom, we have the middle. This is the code that then calculates that;

var xTotal = Enumerable.Range(minX, maxX - minX + 1).Sum();
var yTotal = Enumerable.Range(minY, maxY - minY + 1).Sum();

var xDisplace = -1f;
var yDisplace = 0f;

float xPos = ((float)(xTotal)/(float)gridWidth)+xDisplace;
float yPos = ((float)(yTotal)/(float)gridHeight)+yDisplace;

Unbelievably, I forgot this very basic mathematical process, instead working on coordinate geometry and wondering how this was done. Ironically, the solution was stumbled onto by pure chance, while researching something totally different. Always the way isn't it? Your brain ticking over in the background, constantly thinking about things completely unrelated. I'm convinced that the basis of the theory that will crack hyperspace travel will be conceived while someone is doing the washing up, or in the shower, or ordering a pizza, and wondering how much quicker it can arrive.

The next problem, smoothing

So, that works. But it snaps. The camera just appears elsewhere when a new block is generated. Given that, while debugging, I have the new blocks generating every 2 seconds, it's pretty jarring. You can see this in the video above. This, however, is a solved problem in Unity based on what's termed as lerping. Apparently this is short for Linear Interpolation. To me, it's the noise that a frog makes on impact, but in this case, it's a useful term. It's used for calculating points in between 2 values to create a new data point between the two, based on an additional constant. What this means in Unity, is that, given 2 co-ordinates, it can calculate a linear position between the 2 points as to where it should be. This results in the following code to calculate a new position;

Vector3 targetPos;
...
{
    if(gameObject.transform.position!=targetPos) {
        Vector3 lerpPosition = Vector3.Lerp (gameObject.transform.position, targetPos, moveSpeed);
        gameObject.transform.position = lerpPosition;
    }
}

Placing this as part of the FixedUpdate part of Unity's Monobehaviour, then the camera moves smoothly to the center of the grid. And thus the solution is found. A nice smooth camera update to get me to the middle of the grid. Another ticket closed off. Here's an example of this lovely satisfying slide.

So what did we learn?

Well for starters, don't reinvent the wheel. Trying to solve a simple math problem with coordinate geometry, I was already fighting a losing battle. In the programming world, no matter how good you are at what you do, somewhere out there is a 12 year old with far more money and time than anything else, and they WILL be able to solve your problem in a quicker, more elegant, and more efficient way.. The fortunate bit of that means that there's always someone out there with the solution on Github, StackOverflow, or some other forum. Perhaps hidden away on some nerdy blog written by a person who's just rattling off the fact that they're a tit who couldn't figure out basic maths.

The other thing to take away from this is that Unity has a hell of a lot of functionality. For example, a lot of users attach cameras to the same game object as their player, and have a displacement so it always remains at a relative distance, and moves with the player character. Sadly this didn't apply here, but it's a trick worth remembering. I try to have everything as disconnected as possible. I'm a sucker for microservice design in my day-to-day job, so I try and keep that modularity everywhere. But in some cases, that level of disconnection can be a hinderance. In my case, I've now tied my Level Runner and Camera together. This means that these two need to be kept active in the scene. The level runner has to be in every scene based on my design, so that's not an issue. I just don't want to have too many dependencies on a scene.

What now?

Based on the lovely Trello board, I'm now working on the scoring system. I've basically dropped the requirement for having housing blocks. Instead, the happiness of the citizens is now going to be the scoring system, rather than a requirement to build and balance. Obviously this will likely be revisited lots of times, as I tweak the game to feel more "fun", but I need to iron out a version 1 for now.

Hope you found this insightful. Hey, look, I published 2 things in a month. Look at me go!

Next Post Previous Post