alice alice

Role: Programmer, Game Designer, Level Designer


Tool: Unity, Photoshop


Team: GMTK Game Jam 2024 (Built to Scale), Team of 7


Timeline: 96 hours


Completion Date: August 20, 2024

Role: Programmer, Game Designer, Level Designer


Tool: Unity, Photoshop


Team: GMTK Game Jam 2024 (Built to Scale), Team of 7


Timeline: 96 hours


Completion Date: August 20, 2024

Role: Programmer, Game Designer, Level Designer


Tool: Unity, Photoshop


Team: GMTK Game Jam 2024 (Built to Scale), Team of 7


Timeline: 96 hours


Completion Date: August 20, 2024

Play ->

Play ->

Play ->

GAME DESIGN

GAME DESIGN

GAME DESIGN

design pillars

design pillars

design pillars

Transformation and Control

Transformation and Control

Transformation and Control

Players gain agency by reshaping themselves and the environment, symbolizing adaptability and control.

Players gain agency by reshaping themselves and the environment, symbolizing adaptability and control.

Players gain agency by reshaping themselves and the environment, symbolizing adaptability and control.

Creative Problem-Solving

Creative Problem-Solving

Creative Problem-Solving

Encourages inventive solutions through scaling, linking, and environmental interactions.

Encourages inventive solutions through scaling, linking, and environmental interactions.

Encourages inventive solutions through scaling, linking, and environmental interactions.

Dynamic Level Designs

Dynamic Level Designs

Dynamic Level Designs

Levels challenge players to think spatially, integrating scaling mechanics with strategic setups.

Levels challenge players to think spatially, integrating scaling mechanics with strategic setups.

Levels challenge players to think spatially, integrating scaling mechanics with strategic setups.

mechanics

mechanics

mechanics

PROGRAMMING

PROGRAMMING

PROGRAMMING

Universal Scaling System

Universal Scaling System

Universal Scaling System

The UpdateScaling() method adjusts the player's size and scales tagged objects dynamically while adhering to specific conditions. Scaling for the player is allowed only when grounded and stationary, and stops if the player's scale reaches predefined maximum or minimum bounds. If scaling is valid, the player's size is updated along a calculated axis, factoring in collision constraints—scaling up only when no collisions are present and scaling down regardless of obstacles. The scaling is clamped to ensure the player's size remains within gameplay-appropriate limits, and the camera updates to maintain an optimal view. For tagged objects, scaling depends on their mode (e.g., proportional or vertical), with adjustments clamped to prevent invalid transformations. This design ensures smooth, controlled scaling for both player and objects, enabling dynamic interactions and puzzle-solving while preserving gameplay balance.

Below is a snippet of the code from the class PlayerScale.cs:

The UpdateScaling() method adjusts the player's size and scales tagged objects dynamically while adhering to specific conditions. Scaling for the player is allowed only when grounded and stationary, and stops if the player's scale reaches predefined maximum or minimum bounds. If scaling is valid, the player's size is updated along a calculated axis, factoring in collision constraints—scaling up only when no collisions are present and scaling down regardless of obstacles. The scaling is clamped to ensure the player's size remains within gameplay-appropriate limits, and the camera updates to maintain an optimal view. For tagged objects, scaling depends on their mode (e.g., proportional or vertical), with adjustments clamped to prevent invalid transformations. This design ensures smooth, controlled scaling for both player and objects, enabling dynamic interactions and puzzle-solving while preserving gameplay balance.

Below is a snippet of the code from the class PlayerScale.cs:

The UpdateScaling() method adjusts the player's size and scales tagged objects dynamically while adhering to specific conditions. Scaling for the player is allowed only when grounded and stationary, and stops if the player's scale reaches predefined maximum or minimum bounds. If scaling is valid, the player's size is updated along a calculated axis, factoring in collision constraints—scaling up only when no collisions are present and scaling down regardless of obstacles. The scaling is clamped to ensure the player's size remains within gameplay-appropriate limits, and the camera updates to maintain an optimal view. For tagged objects, scaling depends on their mode (e.g., proportional or vertical), with adjustments clamped to prevent invalid transformations. This design ensures smooth, controlled scaling for both player and objects, enabling dynamic interactions and puzzle-solving while preserving gameplay balance.

Below is a snippet of the code from the class PlayerScale.cs:

private void UpdateScaling()
{
    #region CHECK SCALE CONDITION

    if (!playerMovement.IsGrounded() || Mathf.Abs(playerMovement.horizontal) > 0) return;

    if (Mathf.Abs(transform.localScale.x) > calculatedPlayerMaxScale.x || Mathf.Abs(transform.localScale.x) < calculatedPlayerMinScale.x)
    {
        Debug.Log("Player scale is at max or min");
        return;
    }

    #endregion

    // Can scale freely if no collision, can only scale down if has collision
    if (GetScalingAxis() != 0f && (IsCollisionFree() || GetScalingAxis() < 0f))
    {
        #region SCALE SELF

        Vector3 normalizedPlayerScale = new Vector3(Math.Abs(originalPlayerScale.x), originalPlayerScale.y, originalPlayerScale.z).normalized;
        normalizedPlayerScale.x *= Mathf.Sign(transform.localScale.x);
        transform.localScale += normalizedPlayerScale * GetScalingAxis();

        // Clamp player scale
        if (Math.Abs(transform.localScale.x) > calculatedPlayerMaxScale.x)
        {
            transform.localScale = VecToPlayerFacingDir(calculatedPlayerMaxScale);
        }

        else if (Math.Abs(transform.localScale.x) < calculatedPlayerMinScale.x)
        {
            transform.localScale = VecToPlayerFacingDir(calculatedPlayerMinScale);
        }

        UpdateCamera();
        #endregion
    }

    // update playerIsScaling
    playerIsScaling = GetScalingAxis() != 0;

    #region SCALE TAGGED OBJECT
    if (activeTaggedObject)
    {
        Scalable scalableObject = activeTaggedObject.GetComponent<Scalable>();

        if (scalableObject.isScalable() || GetScalingAxis() < 0f)
        {

            // Scale Proportionally
            switch (scalableObject.scaleOption)
            {
                case ScaleOption.PROPORTIONAL:
                    Vector3 objectOriginalScale = scalableObject.originalScale;

                    // normalize the original scale using the length
                    Vector3 normalizedOriginalScale = new Vector3(Math.Abs(objectOriginalScale.x), objectOriginalScale.y, objectOriginalScale.z).normalized;

                    // scale the active object
                    activeTaggedObject.transform.localScale += VecToActiveObjFacingDir(normalizedOriginalScale) * GetScalingAxis();

                    // clamp active object scale
                    if (scalableObject.calculatedMaxScale.x < Mathf.Abs(activeTaggedObject.transform.localScale.x))
                    {
                        activeTaggedObject.transform.localScale = VecToActiveObjFacingDir(scalableObject.calculatedMaxScale);
                    }

                    if (scalableObject.calculatedMinScale.x > Mathf.Abs(activeTaggedObject.transform.localScale.x))
                    {
                        activeTaggedObject.transform.localScale = VecToActiveObjFacingDir(scalableObject.calculatedMinScale);
                    }

                    break;
                case ScaleOption.VERTICAL:
                    activeTaggedObject.transform.localScale += Vector3.up * GetScalingAxis();

                    // clamp active object scale
                    if (scalableObject.calculatedMaxScale.y < Mathf.Abs(activeTaggedObject.transform.localScale.y))
                    {
                        Debug.Log("Clamping object scale - max");
                        activeTaggedObject.transform.localScale = VecToActiveObjFacingDir(scalableObject.calculatedMaxScale);
                    }

                    if (scalableObject.calculatedMinScale.y > Mathf.Abs(activeTaggedObject.transform.localScale.y))
                    {
                        Debug.Log("Clamping object scale - min");
                        activeTaggedObject.transform.localScale = VecToActiveObjFacingDir(scalableObject.calculatedMinScale);
                    }

                    break;
            }
        }
    }
    #endregion

private void UpdateScaling()
{
    #region CHECK SCALE CONDITION

    if (!playerMovement.IsGrounded() || Mathf.Abs(playerMovement.horizontal) > 0) return;

    if (Mathf.Abs(transform.localScale.x) > calculatedPlayerMaxScale.x || Mathf.Abs(transform.localScale.x) < calculatedPlayerMinScale.x)
    {
        Debug.Log("Player scale is at max or min");
        return;
    }

    #endregion

    // Can scale freely if no collision, can only scale down if has collision
    if (GetScalingAxis() != 0f && (IsCollisionFree() || GetScalingAxis() < 0f))
    {
        #region SCALE SELF

        Vector3 normalizedPlayerScale = new Vector3(Math.Abs(originalPlayerScale.x), originalPlayerScale.y, originalPlayerScale.z).normalized;
        normalizedPlayerScale.x *= Mathf.Sign(transform.localScale.x);
        transform.localScale += normalizedPlayerScale * GetScalingAxis();

        // Clamp player scale
        if (Math.Abs(transform.localScale.x) > calculatedPlayerMaxScale.x)
        {
            transform.localScale = VecToPlayerFacingDir(calculatedPlayerMaxScale);
        }

        else if (Math.Abs(transform.localScale.x) < calculatedPlayerMinScale.x)
        {
            transform.localScale = VecToPlayerFacingDir(calculatedPlayerMinScale);
        }

        UpdateCamera();
        #endregion
    }

    // update playerIsScaling
    playerIsScaling = GetScalingAxis() != 0;

    #region SCALE TAGGED OBJECT
    if (activeTaggedObject)
    {
        Scalable scalableObject = activeTaggedObject.GetComponent<Scalable>();

        if (scalableObject.isScalable() || GetScalingAxis() < 0f)
        {

            // Scale Proportionally
            switch (scalableObject.scaleOption)
            {
                case ScaleOption.PROPORTIONAL:
                    Vector3 objectOriginalScale = scalableObject.originalScale;

                    // normalize the original scale using the length
                    Vector3 normalizedOriginalScale = new Vector3(Math.Abs(objectOriginalScale.x), objectOriginalScale.y, objectOriginalScale.z).normalized;

                    // scale the active object
                    activeTaggedObject.transform.localScale += VecToActiveObjFacingDir(normalizedOriginalScale) * GetScalingAxis();

                    // clamp active object scale
                    if (scalableObject.calculatedMaxScale.x < Mathf.Abs(activeTaggedObject.transform.localScale.x))
                    {
                        activeTaggedObject.transform.localScale = VecToActiveObjFacingDir(scalableObject.calculatedMaxScale);
                    }

                    if (scalableObject.calculatedMinScale.x > Mathf.Abs(activeTaggedObject.transform.localScale.x))
                    {
                        activeTaggedObject.transform.localScale = VecToActiveObjFacingDir(scalableObject.calculatedMinScale);
                    }

                    break;
                case ScaleOption.VERTICAL:
                    activeTaggedObject.transform.localScale += Vector3.up * GetScalingAxis();

                    // clamp active object scale
                    if (scalableObject.calculatedMaxScale.y < Mathf.Abs(activeTaggedObject.transform.localScale.y))
                    {
                        Debug.Log("Clamping object scale - max");
                        activeTaggedObject.transform.localScale = VecToActiveObjFacingDir(scalableObject.calculatedMaxScale);
                    }

                    if (scalableObject.calculatedMinScale.y > Mathf.Abs(activeTaggedObject.transform.localScale.y))
                    {
                        Debug.Log("Clamping object scale - min");
                        activeTaggedObject.transform.localScale = VecToActiveObjFacingDir(scalableObject.calculatedMinScale);
                    }

                    break;
            }
        }
    }
    #endregion

The Scalable class controls object scaling, allowing proportional or vertical resizing with adjustable size limits. It calculates the allowed scale range based on the chosen option and checks if scaling is possible using the isScalable() and IsCollisionFree() methods, which verify the object's scale and collision status. Collision detection is done through box colliders positioned around the object. The ResetScalable() function reverts the object to its original scale and position. This approach ensures controlled scaling while preventing unrealistic transformations and maintaining gameplay balance.

The Scalable class controls object scaling, allowing proportional or vertical resizing with adjustable size limits. It calculates the allowed scale range based on the chosen option and checks if scaling is possible using the isScalable() and IsCollisionFree() methods, which verify the object's scale and collision status. Collision detection is done through box colliders positioned around the object. The ResetScalable() function reverts the object to its original scale and position. This approach ensures controlled scaling while preventing unrealistic transformations and maintaining gameplay balance.

The Scalable class controls object scaling, allowing proportional or vertical resizing with adjustable size limits. It calculates the allowed scale range based on the chosen option and checks if scaling is possible using the isScalable() and IsCollisionFree() methods, which verify the object's scale and collision status. Collision detection is done through box colliders positioned around the object. The ResetScalable() function reverts the object to its original scale and position. This approach ensures controlled scaling while preventing unrealistic transformations and maintaining gameplay balance.

using System;
using UnityEngine;


public class Scalable : MonoBehaviour
{
    public float maxScale = 5f;
    public float minScale = 0.5f;
    public ScaleOption scaleOption;
    public LayerMask whatToIgnore;
    public GameObject realScalableObj;
    [HideInInspector] public Vector3 originalScale;
    [HideInInspector] public Vector3 calculatedMinScale;
    [HideInInspector] public Vector3 calculatedMaxScale;

    Vector3 originalPosition;

    void Start()
    {
        originalScale = transform.localScale;
        originalPosition = transform.position;

        switch (scaleOption)
        {
            case ScaleOption.PROPORTIONAL:
                calculatedMinScale = minScale * originalScale;
                calculatedMaxScale = maxScale * originalScale;
                break;
            case ScaleOption.VERTICAL:
                calculatedMinScale = originalScale;
                calculatedMinScale.y = minScale;


                calculatedMaxScale = originalScale;
                calculatedMaxScale.y = maxScale;
                break;
        }
    }

    public bool isScalable()
    {
        if (!IsCollisionFree())
        {
            return false;
        }

        switch (scaleOption)
        {
            case ScaleOption.PROPORTIONAL:
                if (Mathf.Abs(transform.localScale.x) > calculatedMaxScale.x
                || Mathf.Abs(transform.localScale.x) < calculatedMinScale.x)
                {
                    return false;
                }

                if (transform.localScale.y > calculatedMaxScale.y
                || transform.localScale.y < calculatedMinScale.y)
                {
                    return false;
                }

                return true;

            case ScaleOption.VERTICAL:
                if (transform.localScale.y > calculatedMaxScale.y
                || transform.localScale.y < calculatedMinScale.y)
                {
                    return false;
                }

                if (transform.localScale.y > calculatedMaxScale.y
                || transform.localScale.y < calculatedMinScale.y)
                {
                    return false;
                }

                return true;
        }

        return false;


    }

    public bool IsCollisionFree()
    {
        Collider2D leftCol = Physics2D.OverlapBox(GetObjEdgePos(-1, 0), GetVerticalBoxSize(), 0, ~whatToIgnore);
        Collider2D rightCol = Physics2D.OverlapBox(GetObjEdgePos(1, 0), GetVerticalBoxSize(), 0, ~whatToIgnore);
        Collider2D topCol = Physics2D.OverlapBox(GetObjEdgePos(0, 1), GetHorizontalBoxSize(), 0, ~whatToIgnore);

        bool leftFree = !leftCol;
        bool rightFree = !rightCol;
        bool topFree = !topCol;

        bool canScale;
        if (scaleOption == ScaleOption.PROPORTIONAL)
        {
            canScale = (leftFree || rightFree) && topFree;
        }
        else
        {
            canScale = topFree;
        }

        return canScale;
    }

    // Get box size for left and right
    private Vector2 GetVerticalBoxSize()
    {
        return new Vector2(0.05f, 0.7f * transform.localScale.y);
    }

    // Get box size for top
    private Vector2 GetHorizontalBoxSize()
    {
        return new Vector2(0.8f * Math.Abs(transform.localScale.x), 0.05f);
    }

    private Vector2 GetObjEdgePos(int xDir, int yDir)
    {
        Vector3 dir = new Vector3(xDir, yDir, 0);
        float posOffset = 0;
        if (xDir != 0)
        {
            posOffset = Math.Abs(transform.localScale.x) / 2;
        }
        else if (yDir != 0)
        {
            posOffset = Math.Abs(transform.localScale.y) / 2;
        }
        return transform.position + dir * posOffset;
    }

    public void ResetScalable()
    {
        transform.localScale = originalScale;
        transform.position = originalPosition;
    }
}

public enum ScaleOption
{
    PROPORTIONAL,
    VERTICAL

using System;
using UnityEngine;


public class Scalable : MonoBehaviour
{
    public float maxScale = 5f;
    public float minScale = 0.5f;
    public ScaleOption scaleOption;
    public LayerMask whatToIgnore;
    public GameObject realScalableObj;
    [HideInInspector] public Vector3 originalScale;
    [HideInInspector] public Vector3 calculatedMinScale;
    [HideInInspector] public Vector3 calculatedMaxScale;

    Vector3 originalPosition;

    void Start()
    {
        originalScale = transform.localScale;
        originalPosition = transform.position;

        switch (scaleOption)
        {
            case ScaleOption.PROPORTIONAL:
                calculatedMinScale = minScale * originalScale;
                calculatedMaxScale = maxScale * originalScale;
                break;
            case ScaleOption.VERTICAL:
                calculatedMinScale = originalScale;
                calculatedMinScale.y = minScale;


                calculatedMaxScale = originalScale;
                calculatedMaxScale.y = maxScale;
                break;
        }
    }

    public bool isScalable()
    {
        if (!IsCollisionFree())
        {
            return false;
        }

        switch (scaleOption)
        {
            case ScaleOption.PROPORTIONAL:
                if (Mathf.Abs(transform.localScale.x) > calculatedMaxScale.x
                || Mathf.Abs(transform.localScale.x) < calculatedMinScale.x)
                {
                    return false;
                }

                if (transform.localScale.y > calculatedMaxScale.y
                || transform.localScale.y < calculatedMinScale.y)
                {
                    return false;
                }

                return true;

            case ScaleOption.VERTICAL:
                if (transform.localScale.y > calculatedMaxScale.y
                || transform.localScale.y < calculatedMinScale.y)
                {
                    return false;
                }

                if (transform.localScale.y > calculatedMaxScale.y
                || transform.localScale.y < calculatedMinScale.y)
                {
                    return false;
                }

                return true;
        }

        return false;


    }

    public bool IsCollisionFree()
    {
        Collider2D leftCol = Physics2D.OverlapBox(GetObjEdgePos(-1, 0), GetVerticalBoxSize(), 0, ~whatToIgnore);
        Collider2D rightCol = Physics2D.OverlapBox(GetObjEdgePos(1, 0), GetVerticalBoxSize(), 0, ~whatToIgnore);
        Collider2D topCol = Physics2D.OverlapBox(GetObjEdgePos(0, 1), GetHorizontalBoxSize(), 0, ~whatToIgnore);

        bool leftFree = !leftCol;
        bool rightFree = !rightCol;
        bool topFree = !topCol;

        bool canScale;
        if (scaleOption == ScaleOption.PROPORTIONAL)
        {
            canScale = (leftFree || rightFree) && topFree;
        }
        else
        {
            canScale = topFree;
        }

        return canScale;
    }

    // Get box size for left and right
    private Vector2 GetVerticalBoxSize()
    {
        return new Vector2(0.05f, 0.7f * transform.localScale.y);
    }

    // Get box size for top
    private Vector2 GetHorizontalBoxSize()
    {
        return new Vector2(0.8f * Math.Abs(transform.localScale.x), 0.05f);
    }

    private Vector2 GetObjEdgePos(int xDir, int yDir)
    {
        Vector3 dir = new Vector3(xDir, yDir, 0);
        float posOffset = 0;
        if (xDir != 0)
        {
            posOffset = Math.Abs(transform.localScale.x) / 2;
        }
        else if (yDir != 0)
        {
            posOffset = Math.Abs(transform.localScale.y) / 2;
        }
        return transform.position + dir * posOffset;
    }

    public void ResetScalable()
    {
        transform.localScale = originalScale;
        transform.position = originalPosition;
    }
}

public enum ScaleOption
{
    PROPORTIONAL,
    VERTICAL

Magnets

Magnets

Magnets

The Magnet class simulates a magnetic force between the magnet object and a specified metal object. It calculates the force applied to the metal object based on the relative sizes (areas) of the magnet and metal, as well as their positions. The magnetic force is proportional to the magnet's area divided by the metal's area, scaled by a magneticForce multiplier. The script applies a force to the metal object in the direction from its closest point to the magnet, creating a realistic attraction effect. This approach ensures that larger magnets exert a stronger pull and accounts for collision bounds to improve precision.

The Magnet class simulates a magnetic force between the magnet object and a specified metal object. It calculates the force applied to the metal object based on the relative sizes (areas) of the magnet and metal, as well as their positions. The magnetic force is proportional to the magnet's area divided by the metal's area, scaled by a magneticForce multiplier. The script applies a force to the metal object in the direction from its closest point to the magnet, creating a realistic attraction effect. This approach ensures that larger magnets exert a stronger pull and accounts for collision bounds to improve precision.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Magnet : MonoBehaviour
{
    public GameObject metalObj;
    public float magneticForce;

    private Rigidbody2D rb;
    private Collider2D col;

    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        col = GetComponent<Collider2D>();
    }

    // Update is called once per frame
    void Update()
    {
        Rigidbody2D metalRb = metalObj.GetComponent<Rigidbody2D>();
        Collider2D metalCollider = metalObj.GetComponent<Collider2D>();

        float magnetArea = CalcArea(gameObject);
        float metalArea = CalcArea(metalObj.gameObject);

        Vector2 metalDir = (V3ToV2(transform.position) - metalCollider.ClosestPoint(transform.position)).normalized;

        // Force is stronger if magnet is larger
        if (metalArea != 0)
        {
            float forceMultiplier = magneticForce * (magnetArea / metalArea);

            metalRb.AddForce(metalDir * forceMultiplier * Time.deltaTime * 100);
        }

    }

    private float CalcArea(GameObject obj)
    {
        Collider2D collider = obj.GetComponent<Collider2D>();
        return collider.bounds.size.x * collider.bounds.size.y;
    }

    private Vector2 V3ToV2(Vector3 v)
    {
        return new Vector2(v.x, v.y);
    }

    private Vector3 V2ToV3(Vector2 v)
    {
        return new Vector3(v.y, v.x, 0

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Magnet : MonoBehaviour
{
    public GameObject metalObj;
    public float magneticForce;

    private Rigidbody2D rb;
    private Collider2D col;

    // Start is called before the first frame update
    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        col = GetComponent<Collider2D>();
    }

    // Update is called once per frame
    void Update()
    {
        Rigidbody2D metalRb = metalObj.GetComponent<Rigidbody2D>();
        Collider2D metalCollider = metalObj.GetComponent<Collider2D>();

        float magnetArea = CalcArea(gameObject);
        float metalArea = CalcArea(metalObj.gameObject);

        Vector2 metalDir = (V3ToV2(transform.position) - metalCollider.ClosestPoint(transform.position)).normalized;

        // Force is stronger if magnet is larger
        if (metalArea != 0)
        {
            float forceMultiplier = magneticForce * (magnetArea / metalArea);

            metalRb.AddForce(metalDir * forceMultiplier * Time.deltaTime * 100);
        }

    }

    private float CalcArea(GameObject obj)
    {
        Collider2D collider = obj.GetComponent<Collider2D>();
        return collider.bounds.size.x * collider.bounds.size.y;
    }

    private Vector2 V3ToV2(Vector3 v)
    {
        return new Vector2(v.x, v.y);
    }

    private Vector3 V2ToV3(Vector2 v)
    {
        return new Vector3(v.y, v.x, 0

Copyright © 2024, Andy Pang. All rights reserved.

Copyright © 2024, Andy Pang. All rights reserved.

Copyright © 2024, Andy Pang. All rights reserved.