



Doughy Dilemma
Role
Solo Developer
Team Size
1
Duration
5 weeks
Tools
Unity, Blender, Github, Adobe Illustrator, Trello, Figma, Yarn
Unity, Blender,
Github, Adobe Illustrator,
Trello, Figma, Yarn
Overview
I embarked on a personal project where I developed a game titled "Doughy Dilemma", drawing inspiration from "A Short Hike". Here's a glimpse into the planning, production, and iteration stages of this passion project.
I embarked on a personal project where I developed a game titled "Doughy Dilemma", drawing inspiration from "A Short Hike". Here's a glimpse into the planning, production, and iteration stages of this passion project.
Planning & Prototyping
The project kicked off with a comprehensive week of planning starting with the Game Design Document and Level Design Document where I created flowcharts to visualize the player mechanics. I utilized Trello to help establish a clear roadmap for the project.
This transition involved documenting each player state and crafting a flowchart illustrating the conditions necessary for state transitions and their respective outcomes.
The project kicked off with a comprehensive week of planning starting with the Game Design Document and Level Design Document where I created flowcharts to visualize the player mechanics. I utilized Trello to help establish a clear roadmap for the project.
This transition involved documenting each player state and crafting a flowchart illustrating the conditions necessary for state transitions and their respective outcomes.


States

State transitions


States


State Transitions


State Machine
Production
The decision to implement a state machine proved pivotal for maintaining a modular and organized character state management system. This choice enhanced scalability, eased maintenance, and debugging compared to a large script.
The decision to implement a state machine proved pivotal for maintaining a modular and organized character state management system. This choice enhanced scalability, eased maintenance, and debugging compared to a large script.
Walking State
Biking State
Jumping State
Jetpack State
Climbing State


Walking State


Biking State


Jumping State
State Runner
To implement the state machine, I had to familiarize myself with abstract classes and how to inherit them in the character controller.
To implement the state machine, I had to familiarize myself with abstract classes and how to inherit them in the character controller.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
namespace StateMachine
{
// StateRunner is a MonoBehaviour that controls the lifecycle and transitions of various states.
// This is a generic class that can handle any state for a given MonoBehaviour.
public abstract class StateRunner<T> : MonoBehaviour where T : MonoBehaviour
{
// List of available states for this runner.
[SerializeField]
private List<State<T>> _states;
// Dictionary mapping each state's type to its instance for fast access.
private readonly Dictionary<Type, State<T>> _stateByType = new();
// The currently active state this runner is in.
private State<T> _activeState;
// On Awake, we initialize our state dictionary and set the first state as the active state.
protected virtual void Awake()
{
// Populate the dictionary with each state's type as the key and the state itself as the value.
_states.ForEach(s => _stateByType.Add(s.GetType(), s));
// Set the initial state to the first state in our list.
SetState(_states[0].GetType());
}
// Method to transition to a new state using the state's type.
public void SetState(Type newStateType)
{
// If there's an active state, call its Exit method to handle any state-exit logic.
if (_activeState != null)
{
_activeState.Exit();
}
// Set the new state as the active state and initialize it.
_activeState = _stateByType[newStateType];
_activeState.Init(GetComponent<T>());
}
// Unity's Update method where we handle input and any update logic for the active state.
private void Update()
{
_activeState.CaptureInput();
_activeState.Update();
}
// Unity's FixedUpdate method where we potentially change states and handle any fixed update logic.
private void FixedUpdate()
{
_activeState.ChangeState();
_activeState.FixedUpdate();
}
}
}
State Runner that CharacterCtrl inherits from
using UnityEngine;
using UnityEngine.InputSystem;
namespace StateMachine
{
public class CharacterCtrl : StateRunner<CharacterCtrl>
{
[Header("Reference Other Scripts")]
public PlayerMovementBigActionMap PMBA;
public GameManager GM;
public InputHandler IH { get; private set; }
[SerializeField] AmIGrounded _AIG;
public IsFacingWall _IFW;
public Jumping _jumpingSC;
[Header("GameObjects")]
[SerializeField] private GameObject playerprefab;
[SerializeField] private GameObject ClimbingPrefab;
[Header("Components")]
[SerializeField] public Rigidbody playerRb;
[SerializeField] Animator playerAnim;
[SerializeField] Animator _JetPackAnim;
private PlayerInput playerinput;
[SerializeField] private CapsuleCollider playercollider;
[Header("Animation")]
int moveAnimationID;
int moveWithBagId;
public bool _isGrounded;
[Header("Floats")]
[SerializeField] private float onGroundMoveForceSlow;
[SerializeField] private float onGroundMoveForceNormal;
[SerializeField] private float OnGroundRun;
[SerializeField] private float _rotationSpeed;
[Header("Jumping")]
public float jumpForce = 10f;
public float maxJumpTime = 1f;
public float fallMultiplier = 2.5f;
public float jumpTimer = 0.2f;
public int maxJumps = 2;
public int jumpCount = 0;
public bool isJumpingPressed;
public bool jump;
[Header("Bike")]
[Header("Climbing")]
[SerializeField] public bool readyToClimb;
[SerializeField] private float climbspeed;
[SerializeField] private float wallDistanceOffset;
[SerializeField] private float sideRaycastOffset;
[Header("Transforms")]
[SerializeField] private Transform orientation;
[SerializeField] private Transform _thisObject;
[SerializeField] private Transform _cameraPlayer;
[SerializeField] private Transform groundCheck;
[Header("Vector3")]
//private Vector3 movement;
private Vector3 movementBike;
private Vector3 upDirection = Vector3.up;
[Header("Layers")]
[SerializeField] private LayerMask groundLayer;
[SerializeField] private LayerMask ClimbLayer;
[Header("Slope Handling")]
[SerializeField] private float maxSlopeAngle;
private RaycastHit slopeHit;
//Bike
[SerializeField] private TurnBikeWheels _tB;
[SerializeField] private Transform _bikeOrientation;
[SerializeField] private GameObject _bike;
[SerializeField] private GameObject _fork;
[SerializeField] private float _forkRotationSpeed;
[SerializeField] private float _bikespeed;
[SerializeField] private float _rotationSpeedBike;
[SerializeField] private float _minRotateValueSpeedBike;
[SerializeField] private float _maxRotateValueSpeedBike;
[SerializeField] private float _minWheelTurn;
[SerializeField] private float _maxWheelTurn;
[SerializeField] private float _minValueSpeedBike;
[SerializeField] private float _maxValueSpeedBike;
//JetPack
[SerializeField] private GameObject _JetPack;
// Hand And Feet Transforms
[Header("Transforms")]
[Header("Trackers Bike")]
[SerializeField] private Transform _leftHandBike, _rightHandBike, _leftFootBike, _rightFootBike;
[Header("trackers Hands and Feet")]
[SerializeField] private Transform _leftHand, _rightHand, _leftFoot, _rightFoot;
[Header("Trackers Climbing")]
[SerializeField] private Transform _lefthandClimb, _rightHandClimb, _leftFootClimb, _rightFootClimb;
// Getters And Setters
public Animator PlayerAnimator { get { return playerAnim; } set { playerAnim = value; } }
public Animator JetPackAnimator { get { return _JetPackAnim; } set { _JetPackAnim = value; } }
public CapsuleCollider PlayerCollider { get { return playercollider; } set { playercollider = value; } }
public Rigidbody PlayerRB { get { return playerRb; } set { playerRb = value; } }
public TurnBikeWheels TB { get { return _tB; } set { _tB = value; } }
//public GameManager GM { get { return _GM; } set { _GM = value; } }
public AmIGrounded AIG { get { return _AIG; } }
//Getters And Setters Bools
public bool ISGrounded { get { return _isGrounded; } }
// Getters And Setters GameObjects
public GameObject Bike { get { return _bike; } set { _bike = value; } }
public GameObject Fork { get { return _fork; } set { _fork = value; } }
public GameObject JetPack { get { return _JetPack; } set { _JetPack = value; } }
// Getters And Setters Floats
public float BikeSpeed { get { return _bikespeed; } set { _bikespeed = value; } }
public float ForkRotationSpeed { get { return _forkRotationSpeed; } set { _forkRotationSpeed = value; } }
public float RotationSpeedBike { get { return _rotationSpeedBike; } set { _rotationSpeedBike = value; } }
public float RotationSpeed { get { return _rotationSpeed; } }
public float MinRotateValueSpeedBike { get { return _minRotateValueSpeedBike; } }
public float MaxRotateValueSpeedBike { get { return _maxRotateValueSpeedBike; } }
public float MinWheelTurn { get { return _minWheelTurn; } }
public float MaxWheelTurn { get { return _maxWheelTurn; } }
public float MinValueSpeedBike { get { return _minValueSpeedBike; } }
public float MaxValueSpeedBike { get { return _maxValueSpeedBike; } }
public float OnGroundMoveForceNormal { get { return onGroundMoveForceNormal; } }
public float OnGroundMoveForceSlow { get { return onGroundMoveForceSlow; } }
public float OnGroundRunning { get { return OnGroundRun; } }
public float JumpTimer { get { return jumpTimer; } set { jumpTimer = value; } }
public float JumpForce { get { return jumpForce; } set { jumpForce = value; } }
public float MaxJumpTime { get { return maxJumpTime; } set { maxJumpTime = value; } }
public float FallMultiplier { get { return fallMultiplier; } set { fallMultiplier = value; } }
// Getters And Setters Animation
public int MoveAnimationID { get { return moveAnimationID; } set { moveAnimationID = value; } }
public int MoveWithBagID { get { return moveWithBagId; } set { moveWithBagId = value; } }
public int JumpCount { get { return jumpCount; } set { jumpCount = value; } }
public int MaxJumps { get { return maxJumps; } set { maxJumps = value; } }
// Getters And Setters Transforms General
public Transform CameraPlayer { get { return _cameraPlayer; } }
public Transform ThisObject { get { return _thisObject; } set { _thisObject = value; } }
public Transform BikeOrientation { get { return _bikeOrientation; } }
public Transform Orientation { get { return orientation; } }
//Getters and Setters Hand And Feet Transforms Bikes;
public Transform LeftHandBike { get { return _leftHandBike; } }
public Transform LeftFootBike { get { return _leftFootBike; } }
public Transform RightHandBike { get { return _rightHandBike; } }
public Transform RightFootBike { get { return _rightFootBike; } }
//Getters And Setters Hand And Feet Transforms
public Transform _LeftHand { get { return _leftHand; } set { _leftHand = value; } }
public Transform _LeftFoot { get { return _leftFoot; } set { _leftFoot = value; } }
public Transform _RightHand { get { return _rightHand; } set { _rightHand = value; } }
public Transform _RightFoot { get { return _rightFoot; } set { _rightHand = value; } }
protected override void Awake()
{
base.Awake();
IH = GetComponent<InputHandler>();
moveAnimationID = Animator.StringToHash("Move");
moveWithBagId = Animator.StringToHash("moveWithBag");
_IFW = GetComponent<IsFacingWall>();
_jumpingSC = GetComponent<Jumping>();
GM = FindObjectOfType<GameManager>();
}
}
}
Character Controller
Scriptable Objects
I used scriptable objects for the states, organizing them in a list. This approach allowed for real-time adjustments during playtesting, making the development process smoother.
using UnityEngine;
using UnityEngine.InputSystem;
namespace StateMachine
{
// The abstract class State is a ScriptableObject that can represent a state for a MonoBehaviour.
// It's generic, which means it can be used for any MonoBehaviour (indicated by the <T>).
public abstract class State<T> : ScriptableObject where T : MonoBehaviour
{
// _runner is a reference to the MonoBehaviour that is running this state.
protected T _runner;
// Initialize the state with a reference to its parent MonoBehaviour.
public virtual void Init(T parent)
{
_runner = parent;
}
// Abstract method to capture any required input.
// Subclasses must provide an implementation for this.
public abstract void CaptureInput();
// Abstract method for update logic.
// Subclasses must provide an implementation for this.
public abstract void Update();
// Abstract method for fixed update logic.
// Subclasses must provide an implementation for this.
public abstract void FixedUpdate();
// Abstract method to change to another state.
// Subclasses must provide an implementation for this.
public abstract void ChangeState();
// Abstract method for logic when exiting the state.
// Subclasses must provide an implementation for this.
public abstract void Exit();
}
}
State Class all States Inherits from
using UnityEngine;
using UnityEngine.InputSystem;
namespace StateMachine
{
[CreateAssetMenu(menuName = "States/Player/Vehicle/Bike")]
public class BikeState : State<CharacterCtrl>
{
// References to various components and managers
CharacterCtrl _parent;
IsFacingWall _IFW;
AmIGrounded _AIG;
GameManager _GM;
//collider
private CapsuleCollider _playercollider;
// Animator
private Animator _playerAnim;
//Generic Transforms
private Transform _bikeOrientation;
private Transform _cameraPlayer;
private Transform _thisObject;
//Rigidbody off the player
private Rigidbody _playerRB;
//Wheel script
private TurnBikeWheels _TB;
// GameObjects
public GameObject _bike;
public GameObject _fork;
// Transforms on bike for hands and feet
private Transform _leftFootBike;
private Transform _rightFootBike;
private Transform _leftHandBike;
private Transform _rightHandBike;
//Transforms on player Hands And Feet
private Transform _leftHand;
private Transform _leftFoot;
private Transform _rightHand;
private Transform _rightFoot;
//Vectors
private Vector2 _inputVectorOnBike;
private Vector2 _inputVectorOnBikeSpeed;
private Vector2 _bikeMove;
private Vector3 _movementBike;
private Vector2 previousJoystickValue;
// Floats
private float _bikeSpeed;
private float _forkRotationSpeed;
private float _rotationSpeedBike;
private float _rotationSpeed;
private float _minRotateValueSpeedBike;
private float _maxRotateValueSpeedBike;
private float _minWheelTurn;
private float _maxWheelTurn;
private float _minValueSpeedBike;
private float _maxValueSpeedBike;
[SerializeField] private float RunAccelRate;
[SerializeField] private float RunDecelRate;
[SerializeField] private float _timeChangeV;
[SerializeField] private float _timeLeft;
//Bools
private bool _changeVehicle;
public override void Init(CharacterCtrl parent)
{
// Initialization logic when the state is first created
base.Init(parent);
_parent = parent;
_playercollider = parent.PlayerCollider;
_playerAnim = parent.PlayerAnimator;
_playerRB = parent.PlayerRB;
_TB = parent.TB;
_bikeOrientation = parent.BikeOrientation;
_cameraPlayer = parent.CameraPlayer;
_thisObject = parent.ThisObject;
_bike = parent.Bike;
_fork = parent.Fork;
_leftFootBike = parent.LeftFootBike;
_rightFootBike = parent.RightFootBike;
_leftHandBike = parent.LeftHandBike;
_rightHandBike = parent.RightHandBike;
_leftFoot = parent._LeftFoot;
_rightFoot = parent._RightFoot;
_leftHand = parent._LeftHand;
_rightHand = parent._RightHand;
_timeLeft = _timeChangeV;
_bikeSpeed = parent.BikeSpeed;
_forkRotationSpeed = parent.ForkRotationSpeed;
_rotationSpeedBike = parent.RotationSpeedBike;
_rotationSpeed = parent.RotationSpeed;
_minRotateValueSpeedBike = parent.MinRotateValueSpeedBike;
_maxRotateValueSpeedBike = parent.MaxRotateValueSpeedBike;
_minWheelTurn = parent.MinWheelTurn;
_maxWheelTurn = parent.MaxWheelTurn;
_minValueSpeedBike = parent.MinValueSpeedBike;
_maxValueSpeedBike = parent.MaxValueSpeedBike;
}
public override void CaptureInput()
{
}
public override void Update()
{
}
public override void FixedUpdate()
{
// Physics-based update logic
_changeVehicle = _parent.IH.SwitchVehicle1;
// setting the Inputvector of the movement
_inputVectorOnBike = _parent.IH.InputVectorOnBike;
_inputVectorOnBikeSpeed = _parent.IH.InputVectorOnBikeSpeed;
_timeLeft -= Time.deltaTime;
_AIG = _parent.AIG;
_GM = _parent.GM;
//Setting the animation for the bike state
_playerAnim.SetBool("OnBike", true);
// Assigning different body parts off the player to the bike
_leftFoot.transform.position = _leftFootBike.transform.position;
_rightFoot.transform.position = _rightFootBike.transform.position;
_leftHand.transform.position = _leftHandBike.transform.position;
_rightHand.transform.position = _rightHandBike.transform.position;
// turning on the Bike GameObject
_bike.SetActive(true);
// Turning off the Collider on the Player
_playercollider.enabled = false;
//Bike Moves
if (_inputVectorOnBike.magnitude >= .1f)
{
_IFW = _parent._IFW;
float forward = _inputVectorOnBike.y * _bikeSpeed;
float right = _inputVectorOnBike.x * _bikeSpeed;
Vector3 targetSpeed = (!_IFW._isFacingWall() ? _bikeOrientation.forward : Vector3.zero) * forward + _bikeOrientation.right * right;
Vector3 velocity = _playerRB.velocity;
velocity.y = 0;
Vector3 speedDiff = targetSpeed - velocity;
float accelRate = (Mathf.Abs(targetSpeed.magnitude) >= 0.5f) ? RunAccelRate : RunDecelRate;
Vector3 movement = speedDiff * accelRate;
_playerRB.AddForce(movement, ForceMode.Force);
//Rotates bike
Quaternion targetRotation = _thisObject.transform.rotation;
_thisObject.transform.rotation = targetRotation;
float targetAngle = Mathf.Atan2(movement.x, movement.z) * Mathf.Rad2Deg;
targetRotation = Quaternion.Euler(0, targetAngle, 0);
_thisObject.transform.rotation = Quaternion.Slerp(_thisObject.transform.rotation, targetRotation, _rotationSpeedBike * Time.deltaTime);
//Rotates fork of bike
float forkTargetAngle = Mathf.Atan2(movement.x, movement.z) * Mathf.Rad2Deg;
Quaternion forkTargetRotation = Quaternion.Euler(-90, forkTargetAngle - 180, 0);
_fork.transform.rotation = Quaternion.Slerp(_fork.transform.rotation, forkTargetRotation, _forkRotationSpeed * Time.deltaTime);
}
//controlls speed of bike and rotations on pedals and wheels
Vector2 joystickvalue = _inputVectorOnBikeSpeed;
// Calculate the rotation angle based on input
float rotationAngle = (joystickvalue.x + previousJoystickValue.x) * _rotationSpeed * Time.deltaTime;
float rotationAngleBike = (joystickvalue.x + previousJoystickValue.x) * _rotationSpeed * Time.deltaTime;
float rotationAngleTurnWheels = (joystickvalue.x + previousJoystickValue.x) * _rotationSpeed * Time.deltaTime;
// Update the current value based on the rotation angle
_bikeSpeed += rotationAngle;
_rotationSpeedBike += rotationAngleBike;
_TB.RotationSpeed += rotationAngleTurnWheels;
// Clamp the current value within the specified range
_bikeSpeed = Mathf.Clamp(_bikeSpeed, _minValueSpeedBike, _maxValueSpeedBike);
_rotationSpeedBike = Mathf.Clamp(_rotationSpeedBike, _minRotateValueSpeedBike, _maxRotateValueSpeedBike);
_TB.RotationSpeed = Mathf.Clamp(_TB.RotationSpeed, _minWheelTurn, _maxWheelTurn);
// Update the previous joystick value for the next frame
previousJoystickValue = joystickvalue;
}
public override void ChangeState()
{
if (_changeVehicle && !_GM._hasJetPack && _timeLeft <= 0f)
{
_runner.SetState(typeof(IdleState));
}
if (_changeVehicle && _GM._hasJetPack && _timeLeft <= 0f)
{
_runner.SetState(typeof(JetPackState));
}
if (_GM._CamIsActive)
{
_runner.SetState(typeof(PauseState));
}
}
public override void Exit()
{
}
}
}
Bike State
Dialogue
For handling dialogue, I opted for Yarn Spinner due to its user-friendly interface. The ability to edit and test dialogue directly in Visual Studio greatly improved workflow efficiency.
Within the other scripts, I utilized Yarn Commands and Yarn Functions to facilitate seamless communication between Yarn and C#, ensuring that specific dialogues were triggered based on specific conditions.


Result


Editing in Visual Studio Code
Editing in Visual Studio Code
Result
Remaining Programming
The remaining programming included creating signposts to guide the player, developing UI elements such as pause menus and tutorial guides, and implementing a feature that makes the houses move aside when obstructing the player's view.
The remaining programming included creating signposts to guide the player, developing UI elements such as pause menus and tutorial guides, and implementing a feature that makes the houses move aside when obstructing the player's view.


Interactive Sign
Paus Menu




Houses move out of the player’s path, ensuring a clear line of sight
Interactive Sign
Pause Menu
Houses move out of the player’s path, ensuring a clear line of sight
Iterations
Throughout the project I consistently tested the game with external playtesters. The scriptable objects significantly simplified the iteration and debugging processes during playtesting. It was important to me to make the controls feel smooth and responsive which I succeeded with after numerous iterations.
Throughout the project I consistently tested the game with external playtesters. The scriptable objects significantly simplified the iteration and debugging processes during playtesting. It was important to me to make the controls feel smooth and responsive which I succeeded with after numerous iterations.


Final Thoughts
As a solo developer of “Doughy Dilemma” I discovered the limitations of big scripts, pushing me towards the state machine approach. This transition sharpened my skills in modular programming, emphasizing the virtues of scalability and maintainability. The iterative playtesting phase was a teachable moment in debugging and refinement. Using scriptable objects was a vital tool for efficient real-time adjustments. All in all, this project served as a technical bootcamp, expanding my toolkit as a game developer.
As a solo developer of “Doughy Dilemma” I discovered the limitations of big scripts, pushing me towards the state machine approach. This transition sharpened my skills in modular programming, emphasizing the virtues of scalability and maintainability. The iterative playtesting phase was a teachable moment in debugging and refinement. Using scriptable objects was a vital tool for efficient real-time adjustments. All in all, this project served as a technical bootcamp, expanding my toolkit as a game developer.