Game Overview and Demo

Project Overview

Neon Blitz is a simple 4-player 2D platformer game. There is one ball, two goals, and 4 players. The objective is to throw the ball into the opposing team's goal.

The control mechanics are:

  • jumping
  • double-jumping
  • dashing / stunning
  • throwing

The source code for the project can be found here.

Technical Summary

The game was built with Unity and C#, and is played with an Xbox or PS4 controller.

Video

In the near future, this text will be replaced with a video demo of the project.

Challenges

Making the Camera Auto Adjust as Players Move Around

We didn't want the camera to simply display the entire map at once, because the players would become very small and wouldn't feel as comfortable to control. We wanted the camera to focus on the point between all 4 players and the ball, and zoom accordingly as these 5 elements moved apart from one another.

I figured out that finding a point in the middle of the 4 players was as simple as creating an array that contained all the players, and then using vector math to sum their position vectors.

The code looks like this:

Vector3 centre;

foreach (Transform player in players) {

    centre += player.position;

}

Then I added the ball to the  players array, and gave this point's X and Y value to the camera.

For the camera's Z value, I scaled it based on a maximum distance between any two objects in the players array.

Splitting a Countdown Float into an Integer and a Decimal

Calculating a countdown timer was very easy. All I had to do was set the variables

float timer = 0f;

float countdownValue = 0f;

and

float secondsToStart = 5f;

at the top of the file, and run the following code in the Update() function:

coundownValue = (secondsToStart - timer);

timer += Time.deltaTime;

The tricky part was displaying it nicely in the UI.

At first I just displayed the entire float value in the middle of the screen. Unfortunately, since I wasn't using a monospace font, the text would display at different widths every time the text updated (every frame), so centre-aligning it wouldn't work.

The best way to display the text nicely was to separate the text into an integer value and a decimal value, but it took me a few tries before I found the best way to do separate the float value.

Initially I tried creating two new values: one an   int to hold the big number, and the other a   float to hold the number after the decimal point.

This method didn't work very well, and I tried using modulo math to get the values I wanted out of the float, but it was using some inherent rounding, and would only update the integer once the number after the decimal point had passed   .5 so I tried something else. In hindsight, I probably could have used some Mathf functions to get this method to work, but it would have been messy.

Eventually I realized that I could simply convert the float into a string (using .ToString()) and then split it. So I did that, and tried using the .Substring method on the resulting string, and promptly ran into an error.

It turns out that when you convert a float into a string, it won't always be the same length. For example, if the float value is 0, its length as a string will be 1, so the .Substring method will give you an error if you give it any length higher than 0.

Then I found out that formatting a string with  .ToString() was as easy as writing .ToString("0.00"). The final lines of code were:

countdownText1.text = countdownValue.ToString("0.00").Substring(0, 1); countdownText2.text = countdownValue.ToString("0.00").Substring(1, 3);

The resulting output looked like this (in a very early build):

Goal Detection

I spent a long time trying to figure out the best and most efficient way to detect goals.

Collision detection in Unity is most easily done (or possibly only done?) inside a script attached to the object that is colliding with something.

Since the ball would be the object colliding with both goals, I figured it made more sense to detect goal collisions with the ball than having a different script for each goal's collider.

Having the collision detection on the ball instead of the goals allowed me to avoid either writing the same methods twice (once in each goal's script), or making everything in a global script's function static so that it could be called by the goal scripts.

Using the ball for collision detection was definitely the right call, but it introduced some interesting bugs. For instance: when a goal is scored, I was setting the ball object to inactive, which caused the Update() function to no longer run. This became a problem because we were using timer variables to determine how long the "BLUE TEAM SCORED" text should remain on-screen.

Ultimately, we decided to turn off the ball's mesh renderer, disable its physics, and lock its position instead of making it inactive. Doing things this way made the ball invisible while still allowing its Update() function to run.

Debugging and solving this problem was a good experience in weighing the benefits and drawbacks of certain solutions.