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