Uber Shape

Overview

  • Release Date: January 2016
  • Platforms: iOS, tvOS
  • Team Size: 1
  • Project Length: 4 Months
  • Engine: Unity
  • Language: C#
  • Tools: Adobe Suite

In 2015 I was accepted into Apple's Apple TV developer program. As part of the program, I received an Apple TV developer kit, and decided I wanted to make a game for the platform. This desire sparked Uber Shape, a simple side-scrolling game with one-touch controls appropriate for the tvOS remote.

Initial development began mid-2015, with the game being written in native Objective-C and utilizing Apple's SpriteKit 2D framework. It was recently ported to the Unity engine, to allow for future platform expansion as well as the general benefits Unity brings compared to Xcode (mainly a better editor compared to Xcode and my own personal preferences).

The goal of Uber Shape is to last as long as possible without crashing into the platforms. Jumping is done by tapping the screen or hitting a button on the tvOS remote or gamepad. Collecting a shape that is the same as you fills your power bar; when full, you transform into a different shape, and the speed is lowered as a reward. Collecting a shape that is different than you speeds up the game. In addition, coins can be collected to spend on various items in the shop, for example a magnet upgrade that attracts coins towards you as you go by.

Responsibilities

  • All programming and scripting
  • All gameplay design
  • All graphical design
  • All sound design
  • Integrated with iCloud and Game Center through native plugins
  • Integrated with Countly and GameAnalytics
  • Integrated with Jenkins server for automated building and deployment of builds

Day/Night Cycle

A day/night cycle was achieved by cycling through an array of colors, lerping between the current color and the next color. At the end of the array, the last color and first color would be sampled, and the cycle would reset from there.

The sampled color would be saved in a private instance variable for use in other places, namely tinting of background graphics.

Although Unity's Gradient class would have probably been more appropriate for this, it has a hard limit of 8 colors (as of time of writing), and this project called for more than that.

_backgroundColorCycleLerpT += 0.025f * Time.deltaTime;
if (_backgroundColorCycleLerpT >= 1.0f) {
    _backgroundColorCycleLerpT = 0.0f;
    _currentStartColorIndex += 1;
    if (_currentStartColorIndex >= backgroundColors.Length) {
        _currentStartColorIndex = 0;
    }
}

int startColorIndex = _currentStartColorIndex;
int endColorIndex = _currentStartColorIndex + 1 >= backgroundColors.Length ? 0 : _currentStartColorIndex + 1;

Color startColor = backgroundColors[startColorIndex];
Color endColor = backgroundColors[endColorIndex];
_currentBackgroundColor = Color.Lerp(startColor, endColor, _backgroundColorCycleLerpT);

Coin Magnet

The Coin Magnet is an upgrade that attracts coins towards the player as they go by, making them much easier to collect.

First, a distance check is done to see if the coin is close enough to the player, based on the level of the Coin Magnet upgrade purchased (there are 3). If the coin is close enough, a vector is calculated from the coin towards the player, and the coin is moved along this path.

If any of these conditions fail, the default behavior of moving the coin right-to-left is performed instead. Additionally, a property on the coin, "isFadingOut", controls the fade-out animation of the coin when collected. If this is true, no movement is applied to the coin.

foreach (Transform child in _coinsContainer.transform) {
    if (_coinMagnetLevel > 0) {
        float coinMagnetLevel1 = 1.736111111111111f;
        float coinMagnetLevel2 = 2.604166666666667f;
        float coinMagnetLevel3 = 3.472222222222222f;
        float coinMagnetMovementSpeed = 9.027777777777778f;
        float coinDistance = 0.0f;
        switch (_coinMagnetLevel) {
            case 1: {
                    coinDistance = coinMagnetLevel1;
                    break;
                }
            case 2: {
                    coinDistance = coinMagnetLevel2;
                    break;
                }
            case 3: {
                    coinDistance = coinMagnetLevel3;
                    break;
                }
            default: {
                    coinDistance = coinMagnetLevel1;
                    break;
                }
        }

        bool isFadingOut = child.GetComponent<Coin>().isFadingOut;
        if (Vector2.Distance(child.transform.position, _player.transform.position) > coinDistance || isFadingOut) {
            child.transform.localPosition = new Vector3(child.transform.position.x - (movementSpeed * Time.deltaTime), child.transform.position.y, 0.0f);
        } else {
            _player.shouldShowCoinMagnetEffect = true;

            if (!isFadingOut) {
                Vector2 vector = new Vector2(_player.transform.position.x - child.transform.position.x, _player.transform.position.y - child.transform.position.y);
                vector = vector.normalized;
                vector *= coinMagnetMovementSpeed * Time.deltaTime;
                child.transform.localPosition = new Vector3(child.transform.position.x + vector.x, child.transform.position.y + vector.y, 0.0f);
            }
        }

        if (child.transform.localPosition.x < -_screenManager.horizontalExtent - child.GetComponent<BoxCollider2D>().bounds.size.x) {
            childrenToDestroy.Add(child.gameObject);
        }
    } else {
        child.transform.localPosition = new Vector3(child.transform.position.x - (movementSpeed * Time.deltaTime), child.transform.position.y, 0.0f);
        if (child.transform.localPosition.x < -_screenManager.horizontalExtent - child.GetComponent<BoxCollider2D>().bounds.size.x) {
            childrenToDestroy.Add(child.gameObject);
        }
    }
}