Swingball prototype

To follow on from my previous post, I am building a prototype game to validate some of my assumptions about strategic capabilities.

Details

NameSwingball
EngineUnity
Version2020.3.23f1
Details about the prototype

Getting started

The game starts on a start screen. Clicking the start button will take the player to the game.

The start screen.

The player

The main character (MC) is a little ball that’s trying to make it to a goal in 2D space. The MC is attracted to the cursor. The camera follows the player and also leans towards the cursor. There’s an arrow pointing to the goal at all times. There’s also a trail left by the MC.

A prototype level in Swingball.

Once the MC reaches the goal, the game transitions to a score screen. The score screen shows whether the player won or lost and how much time it took. It also gives the player the option to retry.

Obstacles

The MC can encounter obstacles. The MC bounces off white obstacles. The player can also click on a white obstacle to swing from a (very stiff) rope. Releasing the mouse button snaps the rope.

Colliding with a red obstacle ends the game. The player is taken to the score screen and is informed that they lost.

Colliding with red obstacles ends the game.

The MC passes through blue obstacles. They reduce the effect of gravity.

Blue obstacles modify the MC’s trajectory.

How it’s implemented

Unity makes sharing Unity projects like this surprisingly difficult. I decided to:

  • Ensure that the game’s rules are pretty simple to implement with a basic understanding of Unity
  • Share the source code of the Unity project

Here’s the folder structure:

  • Assets
    • Prefabs
      • Background
      • Goal
      • Player
      • Wall
        • DangerWall
        • SwingableWall
        • WaterWall
    • Scenes
      • Data Transfer Objects
    • Scripts
      • Camera
    • UI
      • MainScene
      • ScoreScene
// Assets/Prefabs/Goal
using UnityEngine;
using UnityEngine.SceneManagement;

public class GoalDetectBehaviour : MonoBehaviour
{
    [SerializeField]
    private CircleCollider2D goalCollider;

    private void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.gameObject.CompareTag("Player"))
        {
            PlayerScore.SetScore(win: true, timeSpan: Time.timeSinceLevelLoad);

            SceneManager.LoadScene("ScoreScene", new LoadSceneParameters(LoadSceneMode.Single));
        }
    }
}
// Assets/Prefabs/Player
using UnityEngine;

public class CursorFollowBehaviour : MonoBehaviour
{
    private const float CursorFollowForce = 2.5f;

    private Camera mainCamera;
    private Rigidbody2D rigidBody;

    private void Start()
    {
        mainCamera = Camera.main;
        rigidBody = gameObject.GetComponent<Rigidbody2D>();
    }

    private void FixedUpdate()
    {
        Vector2 direction = (mainCamera.ScreenToWorldPoint(Input.mousePosition) - gameObject.transform.position).normalized;

        rigidBody.AddForce(direction * CursorFollowForce, ForceMode2D.Force);
    }
}
// Assets/Prefabs/Player
using UnityEngine;

public class GoalPointBehaviour : MonoBehaviour
{
    private const float GoalLineLength = 1f;

    private GameObject goal;
    private LineRenderer lineRenderer;
    private Rigidbody2D parentRigidBody;

    private void Start()
    {
        goal = GameObject.FindGameObjectWithTag("Finish");
        lineRenderer = gameObject.GetComponent<LineRenderer>();
        parentRigidBody = gameObject.GetComponentInParent<Rigidbody2D>();
    }

    private void FixedUpdate()
    {
        Vector2 direction = (goal.transform.position - gameObject.transform.position).normalized;

        lineRenderer.SetPosition(1, direction * GoalLineLength);
        gameObject.transform.localRotation = Quaternion.Inverse(parentRigidBody.gameObject.transform.localRotation);
    }
}
// Assets/Prefabs/Player
public class PlayerSwingBehaviour : MonoBehaviour
{
    [SerializeField]
    private HingeJoint2D hingeJoint2d;

    [SerializeField]
    private LineRenderer hingeLine;

    private Camera mainCamera;

    private void Start()
    {
        mainCamera = Camera.main;
    }

    private void Update()
    {
        if (Input.GetMouseButtonUp(0))
        {
            hingeJoint2d.enabled = false;
            hingeJoint2d.anchor = Vector2.zero;
            hingeJoint2d.connectedAnchor = Vector2.zero;
        }

        if (hingeJoint2d.enabled)
        {
            hingeLine.SetPosition(1, hingeJoint2d.anchor);
        }
        else
        {
            hingeLine.SetPosition(1, Vector3.zero);
        }
    }
}
// Assets/Prefabs/Wall/DangerWall
using UnityEngine;
using UnityEngine.SceneManagement;

public class KillPlayerBehaviour : MonoBehaviour
{
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Player"))
        {
            PlayerScore.SetScore(win: false, timeSpan: Time.timeSinceLevelLoad);

            SceneManager.LoadScene("ScoreScene", new LoadSceneParameters(LoadSceneMode.Single));
        }
    }
}
// Assets/Prefabs/Wall/SwingableWall
using UnityEngine;

public class PlayerSwingableBehaviour : MonoBehaviour
{
    private Camera mainCamera;
    private HingeJoint2D playerHinge;

    private void Start()
    {
        mainCamera = Camera.main;

        playerHinge = GameObject
            .FindGameObjectWithTag("Player")
            .GetComponentInParent<HingeJoint2D>();
    }

    private void OnMouseDown()
    {
        Vector2 hingePosition = playerHinge.gameObject.transform.InverseTransformPoint(
            mainCamera.ScreenToWorldPoint(Input.mousePosition));

        playerHinge.anchor = hingePosition;
        playerHinge.connectedAnchor = hingePosition;
        playerHinge.enabled = true;
    }
}
// Assets/Prefabs/Wall/WaterWall
using UnityEngine;

public class FloatPlayerBehaviour : MonoBehaviour
{
    private const float DisabledGravityScale = 0.1f;
    private const float EnabledGravityScale = 1f;

    private void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.gameObject.CompareTag("Player"))
        {
            collision.gameObject.GetComponentInParent<Rigidbody2D>().gravityScale = DisabledGravityScale;
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.gameObject.CompareTag("Player"))
        {
            collision.gameObject.GetComponentInParent<Rigidbody2D>().gravityScale = EnabledGravityScale;
        }
    }
}
// Assets/Scenes/Data Transfer Objects
public static class PlayerScore
{
    public static bool Win { get; private set; }

    public static float TimeSpan { get; private set; }

    public static void SetScore(bool win, float timeSpan)
    {
        Win = win;
        TimeSpan = timeSpan;
    }
}
// Assets/Scripts/Camera
using UnityEngine;

public class PlayerFollowBehaviour : MonoBehaviour
{
    private const float PlayerFollowSpeed = 0.2f;
    private const float MouseFollowMagnitude = 0.07f;

    [SerializeField]
    private GameObject player;

    private Camera mainCamera;

    private void Start()
    {
        mainCamera = Camera.main;
    }

    private void FixedUpdate()
    {
        Vector2 position = gameObject.transform.position;

        position = FollowPlayer(
            position,
            player.transform.position,
            PlayerFollowSpeed);

        position = FollowMouse(position,
            mainCamera.ScreenToWorldPoint(Input.mousePosition),
            MouseFollowMagnitude);

        gameObject.transform.position = new Vector3(position.x, position.y, gameObject.transform.position.z);
    }

    private static Vector2 FollowPlayer(Vector2 position, Vector2 playerPosition, float playerFollowSpeed)
    {
        return Vector2.Lerp(position, playerPosition, playerFollowSpeed);
    }

    private static Vector2 FollowMouse(Vector2 position, Vector2 mousePosition, float mouseFollowMagnitude)
    {
        return Vector2.Lerp(position, mousePosition, mouseFollowMagnitude);
    }
}
// Assets/UI/MainScene
using UnityEngine;
using UnityEngine.SceneManagement;

public class StartButtonBehaviour : MonoBehaviour
{
    public void Click()
    {
        SceneManager.LoadScene("MainScene", new LoadSceneParameters(LoadSceneMode.Single));
    }
}
// Assets/UI/ScoreScene
using UnityEngine;
using UnityEngine.SceneManagement;

public class RetryButtonBehaviour : MonoBehaviour
{
    public void Click()
    {
        SceneManager.LoadScene("MainScene", new LoadSceneParameters(LoadSceneMode.Single));
    }
}
// Assets/UI/ScoreScene
using UnityEngine;
using UnityEngine.UI;

public class UpdateElapsedTimeBehaviour : MonoBehaviour
{
    private Text textComponent;

    private void Start()
    {
        textComponent = gameObject.GetComponent<Text>();
    }

    private void Update()
    {
        textComponent.text = $"Elapsed Time: {PlayerScore.TimeSpan:F2}";
    }
}
// Assets/UI/ScoreScene
using UnityEngine;
using UnityEngine.UI;

public class UpdateScoreBehaviour : MonoBehaviour
{
    private Text textComponent;

    private void Start()
    {
        textComponent = gameObject.GetComponent<Text>();
    }

    private void Update()
    {
        textComponent.text = PlayerScore.Win ? "You won!" : "You lost.";
    }
}